前言
前天ctfshow举办的新手杯,里面的部分web题对于我来说还是有必要记录一下的。里面的大师傅都太强了。
easy_eval
web签到题,打开题目一段代码:
".$code);}
有?>将eval函数进行闭合,eval函数失去了作用,所以我们需要补上php标签,其中我们输入不能有问号。所以用javascript的标签来代替php的,上payload:
成功得到flag。
剪刀石头布 (session反序列化)
题目是与机器人猜拳的游戏,赢一百次得flag。这里直接show source查看源码。 由于代码太多,这里只放关键代码。
ini_set('session.serialize_handler', 'php'); if(isset($_POST['source'])){ highlight_file(__FILE__); phpinfo();
这里在当前页面设置的处理器为php,题目中也给出了phpinfo();页面,我们找出相关配置信息。
这里我们可以发现服务器使用的处理器为php_serialize,与当前页面处理器不同,在反序列化的时候会造成一些问题。同时cleanup配置没开,关闭了session自动清理,所以我们不需要进行条件竞争。并且我们可以通过session上传进度来传递我们的反序列化串。再看我们主要用到的类。
class Game{ public $log,$name,$play; public function __construct($name){ $this->name = $name; $this->log = '/tmp/'.md5($name).'.log'; }public function __destruct(){ echo "Game History
\n"; echo "\n"; echo file_get_contents($this->log); echo ""; } }
在__destruct方法中有file_get_contents函数可以来读取flag.所以这题主要思路就是构造序列化串利用服务器处理器不同造成的安全问题通过 PHP_SESSION_UPLOAD_PROGRESS来提交我们的序列化串。 在这里对session反序列化进行简单说明。网上相关讲解也很多。
php在session存储和读取的时候会自动进行序列化和反序列化,当然,这是前提。而php又有不同的php处理器来处理这些序列化串,主要有三种php处理器。
php_serialize //经过serialize函数序列化数组php //键名+竖线+serialize函数处理的值php_binary //键名对应的长度的ascli字符+键名+serialize序列化的值
这样说还是比较抽象,直接举个例子。
当我们get传入数据后,会在服务器的tmp目录下生成sess_开头的临时文件,查看临时文件得到这样的反序列化数据。
php模式下;
php_binary模式下:
在php模式下|分割了键名和键值,php会反序列化|后面的键值,那么好,回到题目来,我们修改类属性。
log = "/var/www/html/flag.php"; }}$a = new Game();echo serialize($a);//|O:4:"Game":1:{s:3:"log";s:22:"/var/www/html/flag.php";}?>
在服务器php_serialize处理下存储该序列化串,管道符只是一个普通的字符存放在属性值里。而后在读取中又经过了php配置处理,这时候管道符就成了分割键名和键值的分割线了。管道符后面的内容也就被成功的反序列化了,成功达到读取flag的目的。最后就是利用session上传进度提交我们的序列化串了,网上白嫖提交页面。
接着上传文件,抓包改数据包,修改文件名为序列化串。
发包成功得到flag。
baby_pickle
出题人说灵感来自php的字符逃逸,挺会玩。题目直接给了源码。
import base64import pickle, pickletoolsimport uuidfrom flask import Flask, requestapp = Flask(__name__)id = 0flag = "ctfshow{" + str(uuid.uuid4()) + "}"class Rookie(): def __init__(self, name, id): self.name = name self.id = id@app.route("/")def agent_show(): global id id = id + 1 if request.args.get("name"): name = request.args.get("name") else: name = "new_rookie" new_rookie = Rookie(name, id) try: file = open(str(name) + "_info", 'wb') info = pickle.dumps(new_rookie, protocol=0) info = pickletools.optimize(info) file.write(info) file.close() except Exception as e: return "error" with open(str(name)+"_info", "rb") as file: user = pickle.load(file) message = "欢迎来到新手村" + user.name + "
\n" + "只有成为大菜鸡才能得到flag" + "
" return message@app.route("/dacaiji")def get_flag(): name = request.args.get("name") with open(str(name)+"_info", "rb") as f: user = pickle.load(f) if user.id != 0: message = "你不是大菜鸡
" return message else: message = "恭喜你成为大菜鸡
\n" + flag + "
" return message@app.route("/change")def change_name(): name = base64.b64decode(request.args.get("name")) newname = base64.b64decode(request.args.get("newname")) file = open(name.decode() + "_info", "rb") info = file.read() print("old_info ====================") print(info) print("name ====================") print(name) print("newname ====================") print(newname) info = info.replace(name, newname) print(info) file.close() with open(name.decode()+ "_info", "wb") as f: f.write(info) return "success"if __name__ == '__main__': app.run(host='0.0.0.0', port=8888)
flask框架,看路由。/路由下,get方式传入name,同时对id+1,并且对Rookie类进行序列化存放在文件中,在/dacaiji路由下,对文件里的序列化串进行反序列化,判断id,若为0则给出flag。然而id我们不可控,而且id初始不可能为0。接下来就是本题的重点,/change路由。修改name的值,我们先把此代码放在本地运行。
我们输入name为123查看本地生成的文件的内容。
生成了123.info文件。则里面的内容为:
ccopy_reg_reconstructor(c__main__Rookiec__builtin__objectNtR(dVnameV123sVidI2sb.
可以看到这里id是等于2的,后面的sb.可相当于结束序列化流程的。现在我们可控的是name的值,根据php反序列化字符逃逸的思想,我们需要扔掉sVid\nI2\nsb.那么name的值就该为123\nsVid\nI0\nsb.那么这样我们就间接更改了id的值了。那么接下来在/change路由对name进行改值了。
本地测试成功。注意base64编码的时候字符串要换行,而不是添加\n,不然会当成普通字符串了。当然题目也打出来了,也不放截图了。
repairman (parse_str函数变量覆盖)
看了wp有种恍然大悟的感觉。
传入mode=0得到源码:
hello,the user!We may change the mode to repaie the server,please keep it unchanged'; }else{ header('refresh:5;url=index.php?mode=1'); exit; }
代码的逻辑很简单。我们需要走进admin的分支,因为test分支只能通过字母数字执行命令,而且无回显,这貌似是不可能的。那么就需要让$secret等于md5('admin'.$config['secret']),secret变量我们完全可控,只是$config['secret']我们目前还不知道。接下来就是本题的重点了,
if(empty($mode)){ $url = parse_url($_SERVER['REQUEST_URI']); parse_str($url['query']); if(empty($mode)) { echo 'Your mode is the guest!'; }}
我当时就挺纳闷的这段代码有什么用,还是太菜了没往这方面想。parse_str函数是可以进行变量覆盖的。
啥都不说,先本地测试一下。
真的变量覆盖了,那么根据这个方式,我们可以直接覆盖掉$config['secret']的值,也就相当于可控了。那么直接上payload了。
?mode=0&config[secret]=boy&secret=a190a8e2dfc19f8efe1ecb0c4e625994
这就进入到admin模式,接着就是执行命令了,exec($cmd)无回显,当然这对于我们来说已经不算难题了。直接post对cmd传参。
cmd=cat config.php>1.txt
成功得到flag。
结语
还有最后一道web,看了官方wp,有种本能的抗拒。这种题涉及的知识点超出了我的学习范围。还得继续积累知识,向大师傅们学习。
来源地址:https://blog.csdn.net/m0_62422842/article/details/127195300