整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

从JS文件中发现「认证绕过」漏洞

译:h4d35

预估稿费:120RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

前言


本篇文章主要介绍了在一次漏洞悬赏项目中如何利用配置错误挖到一个认证绕过漏洞。

从JS文件中发现认证绕过漏洞


本文内容源自一个私有漏洞赏金计划。在这个漏洞计划中,接受的漏洞范围限于目标网站少数几个公开的功能。基于前期发现的问题(当我被邀请进这个计划时,其他人一共提交了5个漏洞),似乎很难再挖到新的漏洞。同时,在赏金详情中提到了这样一句话:

如果你成功进入管理页面,请立即报告,请勿在/admin中进行进一步的测试。

然而,目标网站中存在一个仅限于未认证和未经授权的用户访问的管理页面。当我们访问/login或/admin时会跳转到https://bountysite.com/admin/dashboard?redirect=/。

对登录页面进行暴力破解也许是一个可行方案,但是我并不喜欢这种方式。看一下网页源码,没什么有用的内容。于是我开始查看目标网站的结构。似乎目标网站的JS文件都放在少数几个文件夹中,如/lib、/js、/application等。

有意思!

祭出神器BurpSuite,使用Intruder跑一下看能否在上述文件夹中找到任何可访问的JS文件。将攻击点设置为https://bountysite.com/admin/dashboard/js/*attack*.js。注意,不要忘记.js扩展名,这样如果文件能够访问则返回200响应。确实有意思!因为我找到了一些可访问的JS文件,其中一个文件是/login.js。

访问这个JS文件https://bountysite.com/admin/dashboard/js/login.js,请求被重定向至管理页面:) 。但是,我并没有查看该文件的权限,只能看到部分接口信息。

但是我并没有就此止步。这看起来很奇怪,为什么我访问一个.js文件却被作为HTML加载了呢?经过一番探查,终于发现,我能够访问管理页面的原因在于*login*。是的,只要在请求路径/dashboard/后的字符串中含有*login*(除了'login',这只会使我回到登录页面),请求就会跳转到这个管理接口,但是却没有正确的授权。

我继续对这个受限的管理接口进行了进一步的测试。再一次查看了页面源码,试着搞清楚网站结构。在这个管理接口中,有其他一些JS文件能够帮助我理解管理员是如何执行操作的。一些管理操作需要一个有效的令牌。我试着使用从一个JS文件中泄露的令牌执行相关管理操作,然并卵。请求还是被重定向到了登录页面。我发现另外一个真实存在的路径中也部署了一些内容,那就是/dashboard/controllers/*.php。

再一次祭出BurpSuite,使用Intruder检查一下是否存在可以从此处访问的其他任何路径。第二次Intruder的结果是,我发现几乎不存在其他无需授权即可访问的路径。这是基于服务器返回的500或者200响应得出的结论。

回到我在上一步侦察中了解到的网站结构中,我发现这些路径是在/controllers中定义的,通过/dashboard/*here*/进行访问。但是直接访问这些路径会跳转到登录页面,似乎网站对Session检查得还挺严格。此时我又累又困,几乎都打算放弃了,但是我想最后再试一把。如果我利用与访问管理页面相同的方法去执行这些管理操作会怎么样呢?很有趣,高潮来了:) 我能够做到这一点。

通过访问/dashboard/photography/loginx,请求跳转到了Admin Photography页面,并且拥有完整的权限!

从这里开始,我能够执行和访问/dashboard/*路径下的所有操作和目录,这些地方充满了诸如SQL注入、XSS、文件上传、公开重定向等漏洞。但是,我没有继续深入测试,因为这些都不在赏金计划之内,根据计划要求,一旦突破管理授权限制,应立即报告问题。此外,根据管理页面显示的调试错误信息可知,我之所以能够访问到管理页面,是因为应用程序在/dashboard/controllers/*文件中存在错误配置。期望达到的效果是:只要请求链接中出现*login*,就重定向至主登录页面,然而,实际情况并不如人所愿。

后记


总之,这是有趣的一天!我拿到了这个漏洞赏金计划最大金额的奖励。

程语言都会需要完善的错误处理策略使得应用程序更为合理的操作错误。错误处理在服务端的处理较为完善,但是浏览器端进展较为缓慢,不同浏览器的错误处理方式也不同,且默认的错误处理方式对用户也不友好。因此,必须理解各种捕获和处理错误的方式。而在 ECMA-262 的第3版中增加了 try-catch 语句块和 throw 语句来处理错误,以及一些错误类型来描述错误。

try-catch

ECMA-262 新增的错误处理方式与 Java 相似,将可能出错的代码放在 try 子句中,而处理错误的代码放在 catch 子句中:

try {
    // Possible error code
} catch (e) {
    // Error Handler
}

catch 子句中捕获到的错误对象包含发生错误的相关信息,通过该错误对象可以获取错误类型的 name 属性和保存错误消息的 message 属性,e.message 即可调用。

在使用 try-catch 语句块时,要知道什么时候使用最好。如果发生无法控制的错误上,就需要使用 try-catch 语句块来处理错误;如果明确知道代码会发生某种错误,就要采用相应的操作来防止错误发生,而不是使用 try-catch 语句块来处理错误。

finally

try-catch 语句块中,无论是 try 子句执行完,还是 catch 子句执行完,都可以接着执行 finally 子句中的代码,trycatch 都无法阻止,包括 return 语句。

function test(){
    try {
        return 1;
    } catch (e){
        throw e;
    } finally {
        return false;
    }
}

上面的方法,最后会返回 false

注意:主要代码中包含了 finally 子句,try 块或 catch 块中的 return 语句就会被忽略,理解这一点很重要。在使用 finally 时一定要仔细确认代码的行为。

抛出错误

使用 throw 抛出错误是另一种错误处理方式。throw 抛出任意表达式。如下所示:

throw "Not Value Error";        // String type error
throw 31;                       // Number type error
throw false;                    // Boolean type error
throw { name: "JavaScript" };   // Object type error

也可以调用 Error 构造函数来生成一个错误对象抛出,接收的值为错误消息。如下所示:

throw new Error("Null value Error.");

也可以调用特定的错误类型生成一个错误对象并抛出。如 SyntaxErrorTypeError 等。使用 ES6 中的继承语法创建错误类型也可以,这样需要提供 name 属性和 message 属性。如下所示:

class CustomError extends Error {
    constructor(message) {
        super(message);
        this.name = "CustomError";
        this.message = message;
    }
}
throw new CustomError("Null Value Error.");

这种方式有助于在捕获错误时更精准地区分错误。

Error

Error 错误类型是基本类型,其它错误类型基本都是继承该类型。Error 提供了一个构造方法用来创建实例对象,当发生错误时,Error 通过构造方法创建实例对象并被抛出。

throw new Error("Null Value Error.");

Error 错误类型提供了三个属性:

  • name:字符串类型。表示错误名称。
  • message:字符串类型。表示错误信息。
  • stack:字符串类型。表示错误的堆栈。

ECMA-262 总共定义了 8 种错误类型,除了 Error 通用的错误类型外,还有如下几种类型:

  • EvalError:该错误类型会在使用 eval() 函数发生错误时抛出。
  • InternalError:该错误类型主要会在底层 JavaScript 引擎抛出异常时由浏览器抛出。如递归过多导致栈溢出。通常并不是需要处理的错误。
  • RangeError:该错误类型会在数值变量或参数越界时抛出。
  • ReferenceError:该错误类型会在引用无效时抛出。
  • SyntaxError:该错误类型会在 eval() 在解析代码的过程中发生。
  • TypeError:该错误类型会在变量或参数不是预期类型,或者访问不存在的方法时抛出。
  • URIError:该错误类型只会在使用 encodeURI()decodeURI() ,并且传入的 URI 格式错误时发生。

浏览器很少会抛出 Error 类型的错误,主要是开发者抛出的自定义错误。而其它错误类型可以使用 instanceof 进行判断。如下所示:

try {
    // Possible error code
} catch (e) {
    if (e instanceof TypeError) {
        // Type Error Handler
    } else if (e instanceof ReferenceError) {
        // Reference Error Handler
    } else {
        // Other Error Handler
    }
}

error 事件

没有被 try-catch 捕获的错误都会在 window 对象上触发 error 事件。该事件会传入几个参数:

  • message:错误消息。
  • url:发生错误的 URL
  • line:错误的行号。
  • colno:错误的列号。
  • errorError对象。
window.onerror = (message, url, line, colno, error) => {
    // 可以对错误的消息进行处理,这里打印在控制台上
    console.log(message);
    return false; // 这里返回 false 来阻止浏览器默认报告错误的行为
};

通过返回 false,函数实际上变成整个文档的 try/catch 语句,可以捕获所有未处理的运行时错误。适当的 try-catch 语句块意味着不会有错误到达浏览器这个 层次,因此就不会触发 error 事件。当返回 true,会阻止执行默认事件处理函数。

而有一些资源加载失败,会触发一个 error 事件,这个事件遵循 DOM 格式。因此可以使用 addEventListener 捕获。

const image = new Image();
image.addEventListener("error", (event) => {
    console.log("Image not loaded!");
});
image.src = "resource.gif"; // 不存在,资源会加载失败。

总结

一个设计良好的错误处理对编程语言十分重要。JavaScript 提供的 try-caththrow 两种处理错误的方式。但是两种错误处理方式,有不同的错误场景要求。如果编写的库或函数需要知道错误发生的具体原因,就抛出异常;如果确切知道接下来的操作,就捕获错误。而没有通过 try-catch 处理的错误,可以使用 window.onerror 事件来处理。

过统计数据库中的1000多个项目,我们发现在 JavaScript 中最常出现的错误有10个。下面会向大家介绍这些错误发生的原因以及如何防止。

对于这些错误发生的次数,我们是通过收集的数据统计得出的。Rollbar 会收集每个项目中的所有错误,并总结每个错误发生的次数,然后通过各个错误的特征进行分组。

下图是发生次数最多的10大 JavaScript 错误:

下面开始深入探讨每个错误发生的情况,以便确定导致错误发生的原因以及如何避免。

1. Uncaught TypeError: Cannot Read Property

这是 JavaScript 开发人员最常遇到的错误。当你读取一个属性或调用一个未定义对象的方法时,Chrome 中就会报出这样的错误。

导致这个错误发生的原因有很多,常见的一种情况是在渲染 UI 组件时,不正确地初始化状态。我们来看一个真实的应用程序中发生这种情况的例子。

以上代码有两个重要方面:

一是组件的状态(例如 this.state),在开始生命周期之前是 undefined 状态。

二是当通过异步的方式获取数据时,无论是在构造函数中 componentWillMount 中,还是在构造函数中提取 componentDidMount,组件在数据加载之前至少会渲染一次。当检测首次渲染时,会发现 this.state.items 是未定义的。此时就会出现一个错误 -“Uncaught TypeError: Cannot read property ‘map’ of undefined" in the consol”。

解决的方法很简单:在构造函数中使用合理的默认值进行状态初始化。

2. TypeError: ‘undefined’ Is Not an Object (evaluating...)

这是在 Safari 中读取属性或调用未定义对象上的方法时发生的错误,这与 Chrome 的上述错误基本相同,只是 Safari 使用不同的错误消息。

3. TypeError: Null Is Not an Object (evaluating...)

这是在 Safari 中读取属性或调用空对象上的方法时发生的错误。

有趣的是,在 JavaScript 中,null 和 undefined 是两种不同的类型,这就是为什么会出现两个不同的错误消息。未定义通常是一个尚未分配的变量,而 null 则表示该值为空。要验证它们不相等,请使用严格的相等运算符:

在实际情况中,导致这种错误的原因之一是:在元素加载之前,就尝试在 JavaScript 中使用 DOM 元素。这是因为 DOM API 对于空白的对象引用返回 null。

任何执行和处理 DOM 元素的 JS 代码,都应该在创建 DOM 元素之后执行。JS 代码按照 HTML 中的规定自上而下进行解释。因此,如果在 DOM 元素之前存在标签,则脚本标签内的 JS 代码就会在浏览器分析 HTML 页面时执行。如果在加载脚本之前尚未创建 DOM 元素,就会出现这样的错误。

在这个例子中,我们可以通过添加一个事件侦听器来解决这个问题,事件侦听器会在页面准备就绪时通知我们。一旦 addEventListener 被触发,该 init( ) 方法就可以使用 DOM 元素。

4. (unknown): Script Error

当未捕获的 JavaScript 错误违背跨边界原则时,就会发生脚本错误。例如,如果将 JavaScript 代码托管在 CDN 上,则任何未被捕获的错误(通过 window.onerror 处理程序发出的错误,而不是 try-catch 中捕获到的错误)将仅报告为“脚本错误”。这是浏览器的一种安全措施,主要用于防止跨域传递数据的情况出现。

要获取真实的错误消息,需要执行以下操作:

Access-Control-Allow-Origin

将 Access-Control-Allow-Origin 设置为 *, 表示可以从任何域正确访问资源。* 如有必要,也可以用自己的域名进行替换,例如:

Access-Control-Allow-Origin: www.example.com。

以下是在各种环境中设置的一些示例:

Apache

在 JavaScript 文件夹中,创建一个 .htaccess 文件,并包含以下内容:

Nginx

将 add_header 指令添加到提供 JavaScript 文件的 location block 中:

HAProxy

将以下内容添加到提供 JavaScript 文件的静态资源配置后端:

在脚本标签上设置crossorigin =“anonymous”

在你的 HTML 源代码中,为每一个脚本设置 Access-Control-Allow-Origin,在设置 SCRIPT 标签中,设置 crossorigin="anonymous"。在将 crossorigin 属性添加到脚本标签之前,请确保正在向脚本文件发送 header。在 Firefox 中,如果 crossorigin 属性存在但 Access-Control-Allow-Origin 标题不存在,则脚本不会执行。

5. TypeError: Object Doesn’t Support Property

当调用未定义的方法时,IE 中会发生这样的错误。

这相当于 Chrome 中的 “undefined’ is not a function” 错误。对于相同的逻辑错误,不同的浏览器可能会有不同的错误消息。

这是在 IE 的 Web 应用程序中使用 JavaScript 命名空间出现的一个常见问题。出现这种情况的绝大部分原因是IE无法将当前名称空间内的方法绑定到this关键字。例如,如果你有 JS Rollbar 方法的命名空间 isAwesome。通常,如果位于 Rollbar 命名空间内,则可以使用以下语法调用该 isAwesome 方法:

Chrome、Firefox 和 Opera 接受这种语法,IE则不接受。因此,使用 JS 命名空间时最安全的做法是:始终以实际名称空间作为前缀。

6. TypeError: ‘undefined’ Is Not a Function

当调用未定义的函数时,Chrome 中就会发生这样的错误。

随着 JavaScript 编码技术和设计模式在过去几年中变得越来越复杂,回调和闭包中的自引用范围也相应增加,这是造成这种混乱现象的主要来源。

正如下面的示例代码片段:

执行上面的代码会导致以下错误:“Uncaught TypeError: undefined is not a function。” 发生以上错误的原因是,当你调用 setTimeout( ) 时,实际上是在调用 window.setTimeout( ),传递给 setTimeout( ) 的匿名函数是在窗口对象的上下文中定义的,而该窗口对象没有 clearBoard( ) 方法。

符合旧版浏览器的解决方案是以变量的方式简单地将引用保存在 this 中,然后通过闭包继承。例如:

或者,在较新的浏览器中,使用 bind( ) 方法传递引用:

7. Uncaught RangeError: Maximum Call Stack

这是在很多种情况,Chrome 中发生的错误,一种情况是当你调用一个不会终止的递归函数时。

如果将值传递给超出范围的函数,也可能会发生这种情况。许多函数只接受特定范围内的数字输入值。例如,Number.toExponential( digits ) 与 Number.toFixed( digits) 接受的参数范围为从0到20,而 Number.toPrecision( digits ) 接受的数字范围为从1至21。

8. TypeError: Cannot Read Property ‘length’

这是 Chrome 中发生的错误,因为读取了未定义长度属性的变量。

通常在数组中能够找到定义的长度,但是如果数组未初始化或变量名在另一个上下文中隐藏,则可能会出现这种错误。让我们用下面的例子来解释这种错误。

当用参数声明一个函数时,这些参数会成为本地参数。这意味着即使你有名称变量 testArray,函数中具有相同名称的参数仍会被视为本地参数。

有两种方法可以解决这个问题:

1. 删除函数声明语句中的参数:

2. 调用传递给我们声明的数组函数:

9. Uncaught TypeError: Cannot Set Property

当尝试访问未定义的变量时,总会返回 undefined。我们也无法获取或设置 undefined 的任何属性。在这种情况下,应用程序将抛出“Uncaught TypeError cannot set property of undefined”。

例如,在 Chrome 浏览器中,如果 test 对象不存在,就会出现这种错误:

所以就需要在访问变量之前,对变量进行定义。

10. ReferenceError: Event Is Not Defined

尝试访问未定义的变量或当前范围之外的变量时会引发此错误。

如果在使用事件处理系统时遇到此错误,请确保使用传入的事件对象作为参数。IE 这样的浏览器提供了全局变量事件,Chrome 会自动将事件变量附加到处理程序中,Firefox 则不会自动添加事件变量。

JavaScript 开发工具推荐

SpreadJS 纯前端表格控件是基于 HTML5 的 JavaScript 电子表格和网格功能控件,提供了完备的公式引擎、排序、过滤、输入控件、数据可视化、Excel 导入/导出等功能,适用于 .NET、Java 和移动端等各平台在线编辑类 Excel 功能的表格程序开发。

结论

事实证明很多这些 null 或 undefined 的错误是普遍存在的。 一个类似于 Typescript 这样的好的静态类型检查系统,当设置为严格的编译选项时,能够帮助开发者避免这些错误。

最后也希望通过本文,可以帮助开发者更好避免或是应对以上的10种错误。


欢迎点击关注GermsTech,这里有最酷的IT、互联网资讯~