Goal Reached Thanks to every supporter — we hit 100%!

Goal: 1000 CNY · Raised: 1310 CNY

100%

CVE-2022-28282 PoC — Mozilla Firefox 资源管理错误漏洞

Source
Associated Vulnerability
Title:Mozilla Firefox 资源管理错误漏洞 (CVE-2022-28282)
Description:By using a link with <code>rel="localization"</code> a use-after-free could have been triggered by destroying an object during JavaScript execution and then referencing the object through a freed pointer, leading to a potential exploitable crash. This vulnerability affects Thunderbird < 91.8, Firefox < 99, and Firefox ESR < 91.8.
Description
PoC for CVE-2022-28282
Readme
# CVE-2022-28282
**Firefox: heap-use-after-free in DocumentL10n::TranslateDocument**
## Old Trick with Simple QL

It's a simple old trick that:  
If we define the Getter of "then" in Object's prototype, Promise->resolve will trigger user's JS callback when it gets the Object as a parameter:
```
https://tc39.es/ecma262/multipage/control-abstraction-objects.html#sec-promise.resolve
```

I wrote a few simple and "violent" rules to search for UAF problems that may be caused by this trick.  
Such as:  
- Callback between two raw pointer 
- A member variable freed in the callback 
- ......  

Like one of them:  

```
from Function magic_func,Function g_resolve,FunctionCall g_setva_fc,VariableAccess va,VariableAccess setnull_va,Expr e_tmp1,Expr e,FunctionCall fc,FunctionCall fc_tmp1,FunctionCall fc_tmp2,FunctionCall fc_tmp3,FunctionCall fc_tmp4,FunctionCall fc_tmp5
where g_resolve.getName().regexpMatch("ResolvePromiseInternal")
and g_resolve.getParentScope().toString().matches("js")
// mMember
and va.getControlFlowScope()=magic_func
and va.getEnclosingFunction() = magic_func
and va.getTarget().getName().regexpMatch("m([A-Z])((.)*)")
and not va.getTarget().getName().matches("mImpl")
// mMember->trigger()->js::ResolvePromiseInternal()
and e.getEnclosingFunction() = magic_func
and fc.getEnclosingFunction() = magic_func
and fc.getTarget().getName().matches("operator->")
and va.getParent() = fc
and fc.getParent() = fc_tmp1
and fc_tmp1.getTarget() = fc_tmp2.getEnclosingFunction()
and fc_tmp2.getTarget() = fc_tmp3.getEnclosingFunction()
and fc_tmp3.getTarget() = fc_tmp4.getEnclosingFunction()
and fc_tmp4.getTarget() = fc_tmp5.getEnclosingFunction()
and fc_tmp5.getTarget() = g_resolve
// exists: mMember = NULL
and setnull_va.getTarget()=va.getTarget()
and g_setva_fc.getTarget().getName().matches("operator=")
and setnull_va.getParent() = g_setva_fc
and e_tmp1.toString().matches("0")
and e_tmp1.getParent() = g_setva_fc
select magic_func,magic_func.getParentScope(),va,g_setva_fc,fc_tmp1
```

**As you can see: this rule is very simple and there are many things that can be improved:**
- Violent way for search 
- Member variables don‘t have to be set to null 
- ......

(But it was indeed an initial version of one of my QLs, and by improving these rules, I found some other interesting things.)

By adjusting the number of levels of FunctionCall recursion, I get a collection of results.  
Unfortunately, there is only one element in the initial result set. Luckily, there is something interesting in this result.  
The result:

```
1  OnParsingCompleted     Document  mDocumentL10n  ......
2  LocalizationLinkAdded  Document  mDocumentL10n  ......
```

## Analyze

```
void Document::LocalizationLinkAdded(Element* aLinkElement) {
  if (!AllowsL10n()) {
    return;
  }
......
......
  if (!mDocumentL10n) {
    Element* elem = GetDocumentElement();
    MOZ_DIAGNOSTIC_ASSERT(elem);

    bool isSync = elem->HasAttr(nsGkAtoms::datal10nsync);
    mDocumentL10n = DocumentL10n::Create(this, isSync);
    ......
  }

  mDocumentL10n->AddResourceId(NS_ConvertUTF16toUTF8(href));

  if (mReadyState >= READYSTATE_INTERACTIVE) {
    mDocumentL10n->TriggerInitialTranslation(); // **** 1 ****
  } else {
......
......
  }
}

void DocumentL10n::TriggerInitialTranslation() {

  ······
  ······
  nsTArray<RefPtr<Promise>> promises;

  ErrorResult rv;
  promises.AppendElement(TranslateDocument(rv));
  if (NS_WARN_IF(rv.Failed())) {
    InitialTranslationCompleted(false);
    mReady->MaybeRejectWithUndefined();
    return;
  }
  promises.AppendElement(TranslateRoots(rv));   // **** 2 ****
  Element* documentElement = mDocument->GetDocumentElement(); // // **** 3 ****
  ......
  ......
}
```

When an HTMLLinkElement(with rel="localization") is added to the document, the document will create a DocumentL10n and trigger TriggerInitialTranslation. In the path from (1) in Document::LocalizationLinkAdded to (2) in DocumentL10n::TriggerInitialTranslation, only the document hold the reference of DocumentL10n in mDocumentL10n.  
In TranslateRoots(2), the function will use a Promise to resolve ErrorResult rv. If we define the Getter of "then" in Object's prototype before, Promise->resolve will trigger user's JS callback in TranslateRoots(2). And we can set HTMLLinkElement's rel to null to trigger Document::LocalizationLinkRemoved. It will set mDocumentL10n to nullptr and remove the reference of DocumentL10n, then we can destroy the object in gc. When the program return to TriggerInitialTranslation, it will lead to a Use-After-Free in (3)(with accessing the member variable: mDocument).

### PoC

Since there is a page privilege check in AllowsL10n, the vulnerability need to be triggered through a Web Extension.

PoC:

```
- manifest.json
- background.js
```


File Snapshot

Log in to view the POC file snapshot cached by Shenlong Bot

Log in to view
Remarks
    1. It is advised to access via the original source first.
    2. Local POC snapshots are reserved for subscribers — if the original source is unavailable, the local mirror is part of the paid plan.
    3. Mirroring, verifying, and maintaining this POC archive takes ongoing effort, so local snapshots are a paid feature. Your subscription keeps the archive online — thank you for the support. View subscription plans →