关于 GoldenDict 的 JavaScript 提权漏洞的思考©🚩🌱

辞书分享 双击取词 便携模式 音频引擎 播放动画 文章缓存 自动分组 阅读模式 问题帮助 划词设置 升级下载

GoldenDict中使用了WebKit处理HTML数据(渲染及互动),基于Qt的WebKit封装为QtWebKit。在C++的接口封装中,QtWebKit中的事件处理同样是基于QObject的信号槽 — 这也是整个Qt库的核心部分。
通过QtWebKit中封装的C++接口,Qt应用可以方便的对Web内容进行操作。反过来,Qt应用可以通过QtWebKit的方法为Web框架注入QObject的类实例(Web Content中的JS对象),以扩展应用程序框架下用户对Web Content数据的处理能力。在用户的HTML内容中,通过调用已注册的JS对象的方法(函数)的数据处理实际上跳转到了应用的Qt槽函数执行。应用程序中所有基于QObject的对象实例,都可以注册为QtWebKit中Web框架实例的JS对象,用户在其JS脚本中都可以调用已注册对象的公有方法 — 这为基于QtWebKit的应用程序提供了强大的自由扩展和处理Web数据的能力,这也是QtWebKit封装的优势所在,让用户可以间接的通过C++方法处理Web Content数据

回到GoldenDict(包括GoldenDict++的早期版本和所有基于GD官方代码的改版),在用户的Web Content中为处理用户浏览词典数据时当前词典的改变,JS脚本中调用了articleViewactiveArticleChanged方法。其中的articleView对象可简单的理解为当前正在浏览的Web视图(标签页的子对象),在新建标签页时即向其Web框架对象中注入了articleView实例,以此处理当用户通过鼠标点击Web页面内容时,如果是从一个词典的视图区域切换到了另一个词典的视图区域,来通知应用程序更新查询结果导航面板中的当前词典(为选中状态)。
在GoldenDict当前的设计中,通过articleView对象开放给用户的接口除了必需的activeArticleChanged外,还有一些辅助接口 — 这些接口用于处理用户对GoldenDict应用的操作或响应数据更新,还有一些派生自QObject对象类的直接或间接的接口,都通过注册articleView对象实例的方式暴漏给了用户(Web Content)— 只要在词典查询的返回数据中含有articleView对象接口的不合理调用,就可能对GoldenDict的运行带来非用户期望的行为。

典型的垃圾词典中的恶意脚本或内容对用户的侵扰一例(请学友们从安全可靠的源获取学习资料)。

使用网站型词典检测是否存在该漏洞(适用于GoldenDict及其衍生版本):

  1. 点击编辑菜单下的词典项,在打开的对话框中添加网站型词典
  2. 填写名称gdissue01, 地址https://www.autoptr.top/service/gdissue01.html
  3. 勾选已启用作为链接;继续添加网站型词典
  4. 填写名称gdissue02, 地址https://www.autoptr.top/service/gdissue02.html
  5. 勾选已启用作为链接;继续添加网站型词典
  6. 填写名称gdissue03, 地址https://www.autoptr.top/service/gdissue03.html
  7. 只勾选已启用;点击对话框下面的确定OK按钮使修改生效
  8. 单独使用gdissue01词典查询任意词汇,如果弹出警告对话框则检测到该漏洞
  9. 新建Tab页并单独使用gdissue02词典查询,如弹出警告对话框则检测到该漏洞
  10. 针对WebEngine版本,单独使用gdissue03查询,如弹出警告对话框则检测到该漏洞

一份测试用例(未有覆盖全部接口)如下:

echo "本脚本中的内容仅用于测试,请勿用于其它用途!<br />"
echo.
echo "<script>setTimeout(function() { articleview.setDisabled(true); articleview.statusBarMessage('测试setDisabled'); },2000);</script>"
echo "<script>setTimeout(function() { articleview.setHidden(true); articleview.statusBarMessage('测试setHidden(true)'); },4000);</script>"
echo "<script>setTimeout(function() { articleview.setHidden(false); articleview.statusBarMessage('测试setHidden(false)'); },6000);</script>"
echo "<script>setTimeout(function() { articleview.zoomOut(); articleview.statusBarMessage('测试zoomOut'); },8000);</script>"
echo "<script>setTimeout(function() { articleview.zoomOut(); articleview.statusBarMessage('测试zoomOut'); },9000);</script>"
echo "<script>setTimeout(function() { articleview.zoomIn(); articleview.statusBarMessage('测试zoomIn'); },10000);</script>"
echo "<script>setTimeout(function() { articleview.zoomIn(); articleview.statusBarMessage('测试zoomIn'); },11000);</script>"
echo "<script>setTimeout(function() { articleview.showDictsPane(); articleview.statusBarMessage('测试showDictsPane'); },12000);</script>"
echo "<script>setTimeout(function() { articleview.sendWordToHistory('测试测试sendWordToHistory历史记录'); articleview.statusBarMessage('测试sendWordToHistory'); },14000);</script>"
echo "<script>setTimeout(function() { articleview.statusBarMessage('测试完成了'); },16000);</script>"
echo "<script>setTimeout(function() { articleview.back(); articleview.statusBarMessage('测试back'); },18000);</script>"
echo 'test PageView issues.'
exit 0

简单一些的JS脚本如下:

<script>
  setTimeout(function() { articleview.setDisabled(true); articleview.statusBarMessage('测试setDisabled'); },2000);
  setTimeout(function() { articleview.setHidden(true); articleview.statusBarMessage('测试setHidden'); },3000);
  setTimeout(function() { articleview.setHidden(false); },5000);
  setTimeout(function() { articleview.zoomOut(); articleview.statusBarMessage('测试zoomOut'); },6000);
  setTimeout(function() { articleview.zoomIn(); articleview.statusBarMessage('测试zoomIn'); },7000);
  setTimeout(function() { articleview.showDictsPane(); articleview.statusBarMessage('测试showDictsPane'); },8000);
  setTimeout(function() { articleview.sendWordToHistory('测试历史记录'); articleview.statusBarMessage('测试sendWordToHistory'); },9000);
  setTimeout(function() { articleview.statusBarMessage('测试完成了'); },9000);
  setTimeout(function() { articleview.back(); articleview.statusBarMessage('测试back'); },10000);
</script> 

上述JS脚本内容可以插入到任何使用了HTML技术的词典内容中去,如mdxzim等格式词典文件。

通常情况下,只需要将用户需要的接口通过QtWebKit的注册方法开放给用户即可,故我们可以只将将必要的接口以单独的QObject派生类来封装,然后将该类实例化并注册给Web框架即可很好的解决上述问题(更新日志)。

上面讲的有些晦涩,也有点儿拗口,但并不影响这个问题的严重性,概括一下就是:

维护:来一把剪刀,天线架上缠绕的的附着物多了,得剪一剪…
仓管:好吧,去仓库找工具箱
维护:嗯~ o( ̄▽ ̄)o…🔧⚓…✂…找着了…
仓管:工具挺多的吧,都拎去,用完了再还回来
维护:好~ o( ̄▽ ̄)o…✂🌼✂🐤✂🐛✂🐙✂🐸…
维护:..✂🌼✂🐤✂🐛✂🐙✂🐸…差不多了…
维护:喔噻,工具多了人不慌……
维护:..咦,🪓~砍一砍… 🔧~转一转… 🔨~敲一敲…
仓管:停,停,停……留着剪刀,其它的都还回来……
…… 修正错误 ……
维护:天线架上缠绕的的附着物又多了,得剪一剪…
仓管:去物架上取剪刀……

通过引入中间对象类对接口进行有效的隔离,很好的就解决了GoldenDict中的JS提权漏洞 — 或者说,这不应该被称为漏洞,因为所有的接口都是有效的,都是被程序需要的,都是在特定的场景下为用户服务的,只是大部分接口都不能被用户直接使用而已。

另一方面这也给我们一些启发,既然为Web框架注册QObject对象是QtWebKit的优点,我们就应该好好使用这一机制。

GoldenDict不支持对HTML5音视频元素,不能播放视频嘛?
调用外部应用来播放不行吗?
对呀,暴风影音、PotrPlayer、MPC,我装了这么多,得用起来才对呀?
好吧,给你再加一方法,可以用来处理视频文件……
……………………

啧~啧~,给JS注册类加一个播放视频文件的公有接口…实现接口功能……
来吧,用起来,GoldenDic中也可以观看词典中的视频内容啦…
看看,GoldenDict脱胎换骨啦,咱们用着爽快,连炸片咸鱼贩子都满腮帮哈喇子瞅着呢……
好吧,最后安利一下:强烈建议所有使用GD++的学友升级到最新版本……